diff --git a/.claude/docs/CODEXLENS_TECHNICAL_SPEC.md b/.claude/docs/CODEXLENS_TECHNICAL_SPEC.md new file mode 100644 index 00000000..a0252ed7 --- /dev/null +++ b/.claude/docs/CODEXLENS_TECHNICAL_SPEC.md @@ -0,0 +1,2099 @@ +# 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/apply-fuzzy-changes.js b/apply-fuzzy-changes.js deleted file mode 100644 index 0bc1d6ce..00000000 --- a/apply-fuzzy-changes.js +++ /dev/null @@ -1,177 +0,0 @@ -import { readFileSync, writeFileSync } from 'fs'; - -const filePath = 'ccw/src/tools/smart-search.js'; -let content = readFileSync(filePath, 'utf8'); - -// 1. Add buildFuzzyRegex function after detectRelationship -const buildFuzzyRegexFunc = ` -/** - * Build fuzzy regex pattern for approximate matching - * @param {string} query - Search query string - * @param {number} maxDistance - Edit distance tolerance (default: 1) - * @returns {string} - Regex pattern suitable for ripgrep -e flag - */ -function buildFuzzyRegex(query, maxDistance = 1) { - const escaped = query.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'); - let pattern; - if (maxDistance === 1) { - pattern = escaped.split('').map(c => { - const upper = c.toUpperCase(); - const lower = c.toLowerCase(); - if (upper !== lower) { - return \`[\${upper}\${lower}]\`; - } - return c; - }).join(''); - } else if (maxDistance === 2) { - pattern = escaped.split('').map(c => \`\${c}?\`).join('.*'); - } else { - pattern = escaped; - } - if (/^[a-zA-Z0-9_]+$/.test(query)) { - pattern = \`\\\\b\${pattern}\\\\b\`; - } - return pattern; -} -`; - -content = content.replace( - /(function detectRelationship\(query\) \{[\s\S]*?\n\})\n\n(\/\*\*\n \* Classify query intent)/, - `$1\n${buildFuzzyRegexFunc}\n$2` -); - -// 2. Add fuzzy parameter to buildRipgrepCommand -content = content.replace( - 'const { query, paths = [\'.\'], contextLines = 0, maxResults = 100, includeHidden = false } = params;', - 'const { query, paths = [\'.\'], contextLines = 0, maxResults = 100, includeHidden = false, fuzzy = false } = params;' -); - -// 3. Replace literal matching line with fuzzy conditional -content = content.replace( - /\/\/ Use literal\/fixed string matching for exact mode\n args\.push\('-F', query\);/, - `// Use fuzzy regex or literal matching based on mode - if (fuzzy) { - args.push('-i', '-e', buildFuzzyRegex(query)); - } else { - args.push('-F', query); - }` -); - -// 4. Add fuzzy case in executeAutoMode -content = content.replace( - /(case 'exact':[\s\S]*?\};\n\n)( case 'fuzzy':\n case 'semantic':)/, - `$1 case 'fuzzy': - // Execute fuzzy mode and enrich result with classification metadata - const fuzzyResult = await executeFuzzyMode(params); - return { - ...fuzzyResult, - metadata: { - ...fuzzyResult.metadata, - classified_as: classification.mode, - confidence: classification.confidence, - reasoning: classification.reasoning - } - }; - - case 'semantic':` -); - -// 5. Replace executeFuzzyMode implementation -const fuzzyModeImpl = `async function executeFuzzyMode(params) { - const { query, paths = [], contextLines = 0, maxResults = 100, includeHidden = false } = params; - - // Check ripgrep availability - if (!checkToolAvailability('rg')) { - return { - success: false, - error: 'ripgrep not available - please install ripgrep (rg) to use fuzzy search mode' - }; - } - - // Build ripgrep command with fuzzy=true - const { command, args } = buildRipgrepCommand({ - query, - paths: paths.length > 0 ? paths : ['.'], - contextLines, - maxResults, - includeHidden, - fuzzy: true - }); - - return new Promise((resolve) => { - const child = spawn(command, args, { - cwd: process.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) => { - const results = []; - - if (code === 0 || (code === 1 && stdout.trim())) { - const lines = stdout.split('\\n').filter(line => line.trim()); - - for (const line of lines) { - try { - const item = JSON.parse(line); - if (item.type === 'match') { - const match = { - file: item.data.path.text, - line: item.data.line_number, - column: item.data.submatches && item.data.submatches[0] ? item.data.submatches[0].start + 1 : 1, - content: item.data.lines.text.trim() - }; - results.push(match); - } - } catch (err) { - continue; - } - } - - resolve({ - success: true, - results, - metadata: { - mode: 'fuzzy', - backend: 'ripgrep-regex', - fuzzy_strategy: 'approximate regex', - count: results.length, - query - } - }); - } else { - resolve({ - success: false, - error: \`ripgrep execution failed with code \${code}: \${stderr}\`, - results: [] - }); - } - }); - - child.on('error', (error) => { - resolve({ - success: false, - error: \`Failed to spawn ripgrep: \${error.message}\`, - results: [] - }); - }); - }); -}`; - -content = content.replace( - /async function executeFuzzyMode\(params\) \{[\s\S]*? \}\n\}/, - fuzzyModeImpl -); - -writeFileSync(filePath, content, 'utf8'); -console.log('Fuzzy mode implementation applied successfully'); diff --git a/apply-fuzzy.py b/apply-fuzzy.py deleted file mode 100644 index 04231425..00000000 --- a/apply-fuzzy.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python3 -import re - -with open('ccw/src/tools/smart-search.js', 'r', encoding='utf-8') as f: - content = f.read() - -# Step 1: Add buildFuzzyRegex after detectRelationship -fuzzy_regex_func = r''' -/** - * Build fuzzy regex pattern for approximate matching - * @param {string} query - Search query string - * @param {number} maxDistance - Edit distance tolerance (default: 1) - * @returns {string} - Regex pattern suitable for ripgrep -e flag - */ -function buildFuzzyRegex(query, maxDistance = 1) { - const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - let pattern; - if (maxDistance === 1) { - pattern = escaped.split('').map(c => { - const upper = c.toUpperCase(); - const lower = c.toLowerCase(); - if (upper !== lower) { - return `[${upper}${lower}]`; - } - return c; - }).join(''); - } else if (maxDistance === 2) { - pattern = escaped.split('').map(c => `${c}?`).join('.*'); - } else { - pattern = escaped; - } - if (/^[a-zA-Z0-9_]+$/.test(query)) { - pattern = `\\b${pattern}\\b`; - } - return pattern; -} -''' - -content = re.sub( - r'(function detectRelationship\(query\) \{[^}]+\})\n\n(/\*\*\n \* Classify)', - r'\1' + fuzzy_regex_func + r'\n\2', - content -) - -# Step 2: Add fuzzy param to buildRipgrepCommand -content = content.replace( - "const { query, paths = ['.'], contextLines = 0, maxResults = 100, includeHidden = false } = params;", - "const { query, paths = ['.'], contextLines = 0, maxResults = 100, includeHidden = false, fuzzy = false } = params;" -) - -# Step 3: Replace literal matching with fuzzy conditional -content = re.sub( - r' // Use literal/fixed string matching for exact mode\n args\.push\(\'-F\', query\);', - r''' // Use fuzzy regex or literal matching based on mode - if (fuzzy) { - args.push('-i', '-e', buildFuzzyRegex(query)); - } else { - args.push('-F', query); - }''', - content -) - -# Step 4: Update executeAutoMode fuzzy case -fuzzy_case = ''' case 'fuzzy': - // Execute fuzzy mode and enrich result with classification metadata - const fuzzyResult = await executeFuzzyMode(params); - return { - ...fuzzyResult, - metadata: { - ...fuzzyResult.metadata, - classified_as: classification.mode, - confidence: classification.confidence, - reasoning: classification.reasoning - } - }; - - case 'semantic':''' - -content = re.sub( - r" case 'fuzzy':\n case 'semantic':", - fuzzy_case, - content -) - -# Step 5: Replace executeFuzzyMode -fuzzy_impl = '''async function executeFuzzyMode(params) { - const { query, paths = [], contextLines = 0, maxResults = 100, includeHidden = false } = params; - - // Check ripgrep availability - if (!checkToolAvailability('rg')) { - return { - success: false, - error: 'ripgrep not available - please install ripgrep (rg) to use fuzzy search mode' - }; - } - - // Build ripgrep command with fuzzy=true - const { command, args } = buildRipgrepCommand({ - query, - paths: paths.length > 0 ? paths : ['.'], - contextLines, - maxResults, - includeHidden, - fuzzy: true - }); - - return new Promise((resolve) => { - const child = spawn(command, args, { - cwd: process.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) => { - const results = []; - - if (code === 0 || (code === 1 && stdout.trim())) { - const lines = stdout.split('\\n').filter(line => line.trim()); - - for (const line of lines) { - try { - const item = JSON.parse(line); - if (item.type === 'match') { - const match = { - file: item.data.path.text, - line: item.data.line_number, - column: item.data.submatches && item.data.submatches[0] ? item.data.submatches[0].start + 1 : 1, - content: item.data.lines.text.trim() - }; - results.push(match); - } - } catch (err) { - continue; - } - } - - resolve({ - success: true, - results, - metadata: { - mode: 'fuzzy', - backend: 'ripgrep-regex', - fuzzy_strategy: 'approximate regex', - count: results.length, - query - } - }); - } else { - resolve({ - success: false, - error: `ripgrep execution failed with code ${code}: ${stderr}`, - results: [] - }); - } - }); - - child.on('error', (error) => { - resolve({ - success: false, - error: `Failed to spawn ripgrep: ${error.message}`, - results: [] - }); - }); - }); -}''' - -content = re.sub( - r'async function executeFuzzyMode\(params\) \{.*? \}\n\}', - fuzzy_impl, - content, - flags=re.DOTALL -) - -with open('ccw/src/tools/smart-search.js', 'w', encoding='utf-8') as f: - f.write(content) - -print('Fuzzy mode implementation applied successfully') diff --git a/ccw/src/tools/smart-search.js b/ccw/src/tools/smart-search.js index 37902877..777b27ad 100644 --- a/ccw/src/tools/smart-search.js +++ b/ccw/src/tools/smart-search.js @@ -34,7 +34,7 @@ function detectLiteral(query) { * Detect regex pattern (contains regex metacharacters) */ function detectRegex(query) { - return /[.*+?^${}()|[\]\]/.test(query); + return /[.*+?^${}()|[\]\\]/.test(query); } /** @@ -111,10 +111,6 @@ function classifyIntent(query) { } -n// Classification confidence threshold -const CONFIDENCE_THRESHOLD = 0.7; - -/** /** * Check if a tool is available in PATH * @param {string} toolName - Tool executable name diff --git a/reference/codanna b/reference/codanna new file mode 160000 index 00000000..80ed5e3d --- /dev/null +++ b/reference/codanna @@ -0,0 +1 @@ +Subproject commit 80ed5e3d5fd37114953f6d237682a7f5c20a9820 diff --git a/reference/code-index-mcp-master/.dockerignore b/reference/code-index-mcp-master/.dockerignore new file mode 100644 index 00000000..7883f309 --- /dev/null +++ b/reference/code-index-mcp-master/.dockerignore @@ -0,0 +1,47 @@ +# Git +.git +.gitignore + +# Python cache files +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific files +.DS_Store +Thumbs.db + +# Code Index MCP specific files +.code_indexer/ + +# Docker files +Dockerfile +.dockerignore diff --git a/reference/code-index-mcp-master/.gitattributes b/reference/code-index-mcp-master/.gitattributes new file mode 100644 index 00000000..74d0c7b2 --- /dev/null +++ b/reference/code-index-mcp-master/.gitattributes @@ -0,0 +1,26 @@ +# Set default behavior to automatically normalize line endings +* text=auto + +# Force specific file types to use LF line endings +*.py text eol=lf +*.js text eol=lf +*.ts text eol=lf +*.json text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.toml text eol=lf +*.txt text eol=lf + +# Force specific file types to use CRLF line endings +*.bat text eol=crlf +*.cmd text eol=crlf + +# Binary files should be left untouched +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.zip binary +*.tar.gz binary \ No newline at end of file diff --git a/reference/code-index-mcp-master/.github/workflows/publish-to-pypi.yml b/reference/code-index-mcp-master/.github/workflows/publish-to-pypi.yml new file mode 100644 index 00000000..6ecbcc13 --- /dev/null +++ b/reference/code-index-mcp-master/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,96 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Release tag (vX.Y.Z) to re-run publish flow' + required: true + type: string + +concurrency: + group: release-${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', github.event.inputs.tag) || github.ref }} + cancel-in-progress: false + +jobs: + verify-and-build: + runs-on: ubuntu-latest + env: + RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', github.event.inputs.tag) || github.ref }} + RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ env.RELEASE_REF }} + + - name: Ensure tag points to default branch + run: | + git fetch origin + TARGET_BRANCH=$(git remote show origin | awk '/HEAD branch/ {print $NF}') + if [ -z "$TARGET_BRANCH" ]; then + TARGET_BRANCH=master + fi + if ! git merge-base --is-ancestor "$(git rev-parse HEAD)" "origin/${TARGET_BRANCH}"; then + echo "::error::Release tag must point to a commit reachable from ${TARGET_BRANCH}" + exit 1 + fi + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install uv + run: python -m pip install --upgrade pip uv + + - name: Cache uv environments + uses: actions/cache@v4 + with: + path: | + .venv + .uv-cache + key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} + restore-keys: | + uv-${{ runner.os }}- + + - name: Install dependencies + run: uv sync --frozen + + - name: Install build tooling + run: uv pip install build twine + + - name: Build distributions + run: uv run python -m build + + - name: Twine check + run: uv run twine check dist/* + + - name: Upload dist artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-${{ env.RELEASE_TAG }} + path: dist/* + retention-days: 7 + + publish: + needs: verify-and-build + runs-on: ubuntu-latest + environment: + name: production + env: + RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist-${{ env.RELEASE_TAG }} + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/reference/code-index-mcp-master/.gitignore b/reference/code-index-mcp-master/.gitignore new file mode 100644 index 00000000..6d33f3c8 --- /dev/null +++ b/reference/code-index-mcp-master/.gitignore @@ -0,0 +1,51 @@ +# Python cache files +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific files +.DS_Store +Thumbs.db + +# Code Index MCP specific files +.code_indexer/ + + +# Claude Code generated files +CLAUDE.local.md +.claude/ +.claude_chat/ +claude_* +COMMIT_MESSAGE.txt +RELEASE_NOTE.txt + +.llm-context/ +AGENTS.md diff --git a/reference/code-index-mcp-master/.pylintrc b/reference/code-index-mcp-master/.pylintrc new file mode 100644 index 00000000..5bc48e5a --- /dev/null +++ b/reference/code-index-mcp-master/.pylintrc @@ -0,0 +1,24 @@ +[MAIN] +# Ignore auto-generated protobuf files +ignore-paths=src/code_index_mcp/scip/proto/scip_pb2.py + +[MESSAGES CONTROL] +# Disable specific warnings for protobuf generated code +disable= + # Generated code warnings + protected-access, + bad-indentation, + line-too-long, + # Other common warnings we might want to disable globally + unused-import, + logging-fstring-interpolation + +[FORMAT] +# Maximum number of characters on a single line +max-line-length=100 + +[DESIGN] +# Maximum number of arguments for function / method +max-args=7 +# Maximum number of locals for function / method body +max-locals=20 \ No newline at end of file diff --git a/reference/code-index-mcp-master/.python-version b/reference/code-index-mcp-master/.python-version new file mode 100644 index 00000000..2c073331 --- /dev/null +++ b/reference/code-index-mcp-master/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/reference/code-index-mcp-master/.well-known/mcp.json b/reference/code-index-mcp-master/.well-known/mcp.json new file mode 100644 index 00000000..d100f27d --- /dev/null +++ b/reference/code-index-mcp-master/.well-known/mcp.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://modelcontextprotocol.io/schemas/mcp.json", + "mcpServers": { + "code-index": { + "command": "uv", + "args": [ + "run", + "code-index-mcp" + ], + "transport": { + "type": "stdio" + }, + "metadata": { + "name": "Code Index MCP", + "description": "Local code-aware MCP server with project indexing, search, and file tools.", + "homepage": "https://github.com/johnhuang316/code-index-mcp", + "capabilities": [ + "code-search", + "symbol-indexing", + "file-system" + ] + } + } + }, + "llmfeed_extension": { + "path": ".well-known/mcp.llmfeed.json" + } +} diff --git a/reference/code-index-mcp-master/.well-known/mcp.llmfeed.json b/reference/code-index-mcp-master/.well-known/mcp.llmfeed.json new file mode 100644 index 00000000..44d1258f --- /dev/null +++ b/reference/code-index-mcp-master/.well-known/mcp.llmfeed.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://modelcontextprotocol.io/schemas/mcp-llmfeed.json", + "feed_type": "mcp_server_list", + "servers": [ + { + "id": "code-index", + "name": "Code Index MCP", + "description": "Exposes project-aware indexing, search, and file utilities for LLM agents via MCP transports.", + "version": "2.9.1", + "transport": "stdio", + "command": "uv", + "args": [ + "run", + "code-index-mcp" + ], + "links": { + "documentation": "https://github.com/johnhuang316/code-index-mcp#readme", + "source": "https://github.com/johnhuang316/code-index-mcp" + }, + "capabilities": [ + "code-search", + "symbol-indexing", + "file-system" + ], + "tags": [ + "fastmcp", + "code-intelligence", + "watcher" + ] + } + ] +} diff --git a/reference/code-index-mcp-master/Dockerfile b/reference/code-index-mcp-master/Dockerfile new file mode 100644 index 00000000..542986b6 --- /dev/null +++ b/reference/code-index-mcp-master/Dockerfile @@ -0,0 +1,24 @@ +# Use lightweight Python image +FROM python:3.11-slim + +# Install git (for code analysis) +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy dependency list and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy code +COPY . . + +# Set Python path +ENV PYTHONPATH="${PYTHONPATH}:/app:/app/src" + +# No default project directory mount point needed, user will explicitly set project path + +# Run MCP tool +# MCP server uses stdio mode by default +ENTRYPOINT ["python", "-m", "code_index_mcp.server"] diff --git a/reference/code-index-mcp-master/LICENSE b/reference/code-index-mcp-master/LICENSE new file mode 100644 index 00000000..adbdd5b9 --- /dev/null +++ b/reference/code-index-mcp-master/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 johnhuang316 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/reference/code-index-mcp-master/README.md b/reference/code-index-mcp-master/README.md new file mode 100644 index 00000000..399f8519 --- /dev/null +++ b/reference/code-index-mcp-master/README.md @@ -0,0 +1,412 @@ +# Code Index MCP + +
+ +[![MCP Server](https://img.shields.io/badge/MCP-Server-blue)](https://modelcontextprotocol.io) +[![Python](https://img.shields.io/badge/Python-3.10%2B-green)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE) + +**Intelligent code indexing and analysis for Large Language Models** + +Transform how AI understands your codebase with advanced search, analysis, and navigation capabilities. + +
+ + + code-index-mcp MCP server + + +## Overview + +Code Index MCP is a [Model Context Protocol](https://modelcontextprotocol.io) server that bridges the gap between AI models and complex codebases. It provides intelligent indexing, advanced search capabilities, and detailed code analysis to help AI assistants understand and navigate your projects effectively. + +**Perfect for:** Code review, refactoring, documentation generation, debugging assistance, and architectural analysis. + +## Quick Start + +### 🚀 **Recommended Setup (Most Users)** + +The easiest way to get started with any MCP-compatible application: + +**Prerequisites:** Python 3.10+ and [uv](https://github.com/astral-sh/uv) + +1. **Add to your MCP configuration** (e.g., `claude_desktop_config.json` or `~/.claude.json`): + ```json + { + "mcpServers": { + "code-index": { + "command": "uvx", + "args": ["code-index-mcp"] + } + } + } + ``` + > Optional: append `--project-path /absolute/path/to/repo` to the `args` array so the server + > initializes with that repository automatically (equivalent to calling `set_project_path` + > after startup). + +2. **Restart your application** – `uvx` automatically handles installation and execution + +3. **Start using** (give these prompts to your AI assistant): + ``` + Set the project path to /Users/dev/my-react-app + Find all TypeScript files in this project + Search for "authentication" functions + Analyze the main App.tsx file + ``` + *If you launch with `--project-path`, you can skip the first command above - the server already + knows the project location.* + +### Codex CLI Configuration + +If you are using Anthropic's Codex CLI, add the server to `~/.codex/config.toml`. +On Windows the file lives at `C:\Users\\.codex\config.toml`: + +```toml +[mcp_servers.code-index] +type = "stdio" +command = "uvx" +args = ["code-index-mcp"] +``` +> You can append `--project-path C:/absolute/path/to/repo` to the `args` list to set the project +> automatically on startup (same effect as running the `set_project_path` tool). + +On Windows, `uvx` needs the standard profile directories to be present. +Keep the environment override in the same block so the MCP starts reliably: + +```toml +env = { + HOME = "C:\\Users\\", + APPDATA = "C:\\Users\\\\AppData\\Roaming", + LOCALAPPDATA = "C:\\Users\\\\AppData\\Local", + SystemRoot = "C:\\Windows" +} +``` + +Linux and macOS already expose the required XDG paths and `HOME`, so you can usually omit the `env` +table there. +Add overrides only if you run the CLI inside a restricted container. + +### FastMCP & Discovery Manifests + +- Run `fastmcp run fastmcp.json` to launch the server via [FastMCP](https://fastmcp.wiki/) with + the correct source entrypoint and dependency metadata. Pass `--project-path` (or call the + `set_project_path` tool after startup) so the index boots against the right repository. +- Serve or copy `.well-known/mcp.json` to share a standards-compliant MCP manifest. Clients that + support the `.well-known` convention (e.g., Claude Desktop, Codex CLI) can import this file + directly instead of crafting configs manually. +- Publish `.well-known/mcp.llmfeed.json` when you want to expose the richer LLM Feed metadata. + It references the same `code-index` server definition plus documentation/source links, which + helps registries present descriptions, tags, and capabilities automatically. + +When sharing the manifests, remind consumers to supply `--project-path` (or to call +`set_project_path`) so the server indexes the intended repository. + +## Typical Use Cases + +**Code Review**: "Find all places using the old API" +**Refactoring Help**: "Where is this function called?" +**Learning Projects**: "Show me the main components of this React project" +**Debugging**: "Search for all error handling related code" + +## Key Features + +### 🔍 **Intelligent Search & Analysis** +- **Dual-Strategy Architecture**: Specialized tree-sitter parsing for 7 core languages, fallback strategy for 50+ file types +- **Direct Tree-sitter Integration**: No regex fallbacks for specialized languages - fail fast with clear errors +- **Advanced Search**: Auto-detects and uses the best available tool (ugrep, ripgrep, ag, or grep) +- **Universal File Support**: Comprehensive coverage from advanced AST parsing to basic file indexing +- **File Analysis**: Deep insights into structure, imports, classes, methods, and complexity metrics after running `build_deep_index` + +### 🗂️ **Multi-Language Support** +- **7 Languages with Tree-sitter AST Parsing**: Python, JavaScript, TypeScript, Java, Go, Objective-C, Zig +- **50+ File Types with Fallback Strategy**: C/C++, Rust, Ruby, PHP, and all other programming languages +- **Document & Config Files**: Markdown, JSON, YAML, XML with appropriate handling +- **Web Frontend**: Vue, React, Svelte, HTML, CSS, SCSS +- **Java Web & Build**: JSP/Tag files (`.jsp`, `.jspx`, `.jspf`, `.tag`, `.tagx`), Grails/GSP (`.gsp`), Gradle & Groovy builds (`.gradle`, `.groovy`), `.properties`, and Protocol Buffers (`.proto`) +- **Database**: SQL variants, NoSQL, stored procedures, migrations +- **Configuration**: JSON, YAML, XML, Markdown +- **[View complete list](#supported-file-types)** + +### ⚡ **Real-time Monitoring & Auto-refresh** +- **File Watcher**: Automatic index updates when files change +- **Cross-platform**: Native OS file system monitoring +- **Smart Processing**: Batches rapid changes to prevent excessive rebuilds +- **Shallow Index Refresh**: Watches file changes and keeps the file list current; run a deep rebuild when you need symbol metadata + +### ⚡ **Performance & Efficiency** +- **Tree-sitter AST Parsing**: Native syntax parsing for accurate symbol extraction +- **Persistent Caching**: Stores indexes for lightning-fast subsequent access +- **Smart Filtering**: Intelligent exclusion of build directories and temporary files +- **Memory Efficient**: Optimized for large codebases +- **Direct Dependencies**: No fallback mechanisms - fail fast with clear error messages + +## Supported File Types + +
+📁 Programming Languages (Click to expand) + +**Languages with Specialized Tree-sitter Strategies:** +- **Python** (`.py`, `.pyw`) - Full AST analysis with class/method extraction and call tracking +- **JavaScript** (`.js`, `.jsx`, `.mjs`, `.cjs`) - ES6+ class and function parsing with tree-sitter +- **TypeScript** (`.ts`, `.tsx`) - Complete type-aware symbol extraction with interfaces +- **Java** (`.java`) - Full class hierarchy, method signatures, and call relationships +- **Go** (`.go`) - Struct methods, receiver types, and function analysis +- **Objective-C** (`.m`, `.mm`) - Class/instance method distinction with +/- notation +- **Zig** (`.zig`, `.zon`) - Function and struct parsing with tree-sitter AST + +**All Other Programming Languages:** +All other programming languages use the **FallbackParsingStrategy** which provides basic file indexing and metadata extraction. This includes: +- **System & Low-Level:** C/C++ (`.c`, `.cpp`, `.h`, `.hpp`), Rust (`.rs`) +- **Object-Oriented:** C# (`.cs`), Kotlin (`.kt`), Scala (`.scala`), Swift (`.swift`) +- **Scripting & Dynamic:** Ruby (`.rb`), PHP (`.php`), Shell (`.sh`, `.bash`) +- **And 40+ more file types** - All handled through the fallback strategy for basic indexing + +
+ +
+🌐 Web & Frontend (Click to expand) + +**Frameworks & Libraries:** +- Vue (`.vue`) +- Svelte (`.svelte`) +- Astro (`.astro`) + +**Styling:** +- CSS (`.css`, `.scss`, `.less`, `.sass`, `.stylus`, `.styl`) +- HTML (`.html`) + +**Templates:** +- Handlebars (`.hbs`, `.handlebars`) +- EJS (`.ejs`) +- Pug (`.pug`) +- FreeMarker (`.ftl`) +- Mustache (`.mustache`) +- Liquid (`.liquid`) +- ERB (`.erb`) + +
+ +
+🗄️ Database & SQL (Click to expand) + +**SQL Variants:** +- Standard SQL (`.sql`, `.ddl`, `.dml`) +- Database-specific (`.mysql`, `.postgresql`, `.psql`, `.sqlite`, `.mssql`, `.oracle`, `.ora`, `.db2`) + +**Database Objects:** +- Procedures & Functions (`.proc`, `.procedure`, `.func`, `.function`) +- Views & Triggers (`.view`, `.trigger`, `.index`) + +**Migration & Tools:** +- Migration files (`.migration`, `.seed`, `.fixture`, `.schema`) +- Tool-specific (`.liquibase`, `.flyway`) + +**NoSQL & Modern:** +- Graph & Query (`.cql`, `.cypher`, `.sparql`, `.gql`) + +
+ +
+📄 Documentation & Config (Click to expand) + +- Markdown (`.md`, `.mdx`) +- Configuration (`.json`, `.xml`, `.yml`, `.yaml`, `.properties`) + +
+ +### 🛠️ **Development Setup** + +For contributing or local development: + +1. **Clone and install:** + ```bash + git clone https://github.com/johnhuang316/code-index-mcp.git + cd code-index-mcp + uv sync + ``` + +2. **Configure for local development:** + ```json + { + "mcpServers": { + "code-index": { + "command": "uv", + "args": ["run", "code-index-mcp"] + } + } + } + ``` + +3. **Debug with MCP Inspector:** + ```bash + npx @modelcontextprotocol/inspector uv run code-index-mcp + ``` + +
+Alternative: Manual pip Installation + +If you prefer traditional pip management: + +```bash +pip install code-index-mcp +``` + +Then configure: +```json +{ + "mcpServers": { + "code-index": { + "command": "code-index-mcp", + "args": [] + } + } +} +``` + +
+ + +## Available Tools + +### 🏗️ **Project Management** +| Tool | Description | +|------|-------------| +| **`set_project_path`** | Initialize indexing for a project directory | +| **`refresh_index`** | Rebuild the shallow file index after file changes | +| **`build_deep_index`** | Generate the full symbol index used by deep analysis | +| **`get_settings_info`** | View current project configuration and status | + +*Run `build_deep_index` when you need symbol-level data; the default shallow index powers quick file discovery.* + +### 🔍 **Search & Discovery** +| Tool | Description | +|------|-------------| +| **`search_code_advanced`** | Smart search with regex, fuzzy matching, file filtering, and paginated results (10 per page by default) | +| **`find_files`** | Locate files using glob patterns (e.g., `**/*.py`) | +| **`get_file_summary`** | Analyze file structure, functions, imports, and complexity (requires deep index) | + +### 🔄 **Monitoring & Auto-refresh** +| Tool | Description | +|------|-------------| +| **`get_file_watcher_status`** | Check file watcher status and configuration | +| **`configure_file_watcher`** | Enable/disable auto-refresh and configure settings | + +### 🛠️ **System & Maintenance** +| Tool | Description | +|------|-------------| +| **`create_temp_directory`** | Set up storage directory for index data | +| **`check_temp_directory`** | Verify index storage location and permissions | +| **`clear_settings`** | Reset all cached data and configurations | +| **`refresh_search_tools`** | Re-detect available search tools (ugrep, ripgrep, etc.) | + +## Usage Examples + +### 🎯 **Quick Start Workflow** + +**1. Initialize Your Project** +``` +Set the project path to /Users/dev/my-react-app +``` +*Automatically indexes your codebase and creates searchable cache* + +**2. Explore Project Structure** +``` +Find all TypeScript component files in src/components +``` +*Uses: `find_files` with pattern `src/components/**/*.tsx`* + +**3. Analyze Key Files** +``` +Give me a summary of src/api/userService.ts +``` +*Uses: `get_file_summary` to show functions, imports, and complexity* +*Tip: run `build_deep_index` first if you get a `needs_deep_index` response.* + +### 🔍 **Advanced Search Examples** + +
+Code Pattern Search + +``` +Search for all function calls matching "get.*Data" using regex +``` +*Finds: `getData()`, `getUserData()`, `getFormData()`, etc.* + +
+ +
+Fuzzy Function Search + +``` +Find authentication-related functions with fuzzy search for 'authUser' +``` +*Matches: `authenticateUser`, `authUserToken`, `userAuthCheck`, etc.* + +
+ +
+Language-Specific Search + +``` +Search for "API_ENDPOINT" only in Python files +``` +*Uses: `search_code_advanced` with `file_pattern: "*.py"` (defaults to 10 matches; use `max_results` to expand or `start_index` to page)* + +
+ +
+Auto-refresh Configuration + +``` +Configure automatic index updates when files change +``` +*Uses: `configure_file_watcher` to enable/disable monitoring and set debounce timing* + +
+ +
+Project Maintenance + +``` +I added new components, please refresh the project index +``` +*Uses: `refresh_index` to update the searchable cache* + +
+ +## Troubleshooting + +### 🔄 **Auto-refresh Not Working** + +If automatic index updates aren't working when files change, try: +- `pip install watchdog` (may resolve environment isolation issues) +- Use manual refresh: Call the `refresh_index` tool after making file changes +- Check file watcher status: Use `get_file_watcher_status` to verify monitoring is active + +## Development & Contributing + +### 🔧 **Building from Source** +```bash +git clone https://github.com/johnhuang316/code-index-mcp.git +cd code-index-mcp +uv sync +uv run code-index-mcp +``` + +### 🐛 **Debugging** +```bash +npx @modelcontextprotocol/inspector uvx code-index-mcp +``` + +### 🤝 **Contributing** +Contributions are welcome! Please feel free to submit a Pull Request. + +--- + +### 📜 **License** +[MIT License](LICENSE) + +### 🌐 **Translations** +- [繁體中文](README_zh.md) +- [日本語](README_ja.md) diff --git a/reference/code-index-mcp-master/README_ja.md b/reference/code-index-mcp-master/README_ja.md new file mode 100644 index 00000000..00319a9f --- /dev/null +++ b/reference/code-index-mcp-master/README_ja.md @@ -0,0 +1,419 @@ +# Code Index MCP + +
+ +[![MCP Server](https://img.shields.io/badge/MCP-Server-blue)](https://modelcontextprotocol.io) +[![Python](https://img.shields.io/badge/Python-3.10%2B-green)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE) + +**大規模言語モデルのためのインテリジェントコードインデックス作成と解析** + +高度な検索、解析、ナビゲーション機能で、AIのコードベース理解を根本的に変革します。 + +
+ + + code-index-mcp MCP server + + +## 概要 + +Code Index MCPは、AIモデルと複雑なコードベースの橋渡しをする[Model Context Protocol](https://modelcontextprotocol.io)サーバーです。インテリジェントなインデックス作成、高度な検索機能、詳細なコード解析を提供し、AIアシスタントがプロジェクトを効果的に理解しナビゲートできるようにします。 + +**最適な用途:**コードレビュー、リファクタリング、ドキュメント生成、デバッグ支援、アーキテクチャ解析。 + +## クイックスタート + +### 🚀 **推奨セットアップ(ほとんどのユーザー)** + +任意MCP対応アプリケーションで開始する最も簡単な方法: + +**前提条件:** Python 3.10+ および [uv](https://github.com/astral-sh/uv) + +1. **MCP設定に追加** (例:`claude_desktop_config.json` または `~/.claude.json`): + ```json + { + "mcpServers": { + "code-index": { + "command": "uvx", + "args": ["code-index-mcp"] + } + } + } + ``` + + > 起動時にプロジェクトを自動設定したい場合は、`args` 配列の末尾に + > `--project-path /absolute/path/to/repo` を追加してください。これで起動直後に + > `set_project_path` を呼び出した場合と同じ状態になります。 +2. **アプリケーションを再起動** – `uvx`がインストールと実行を自動処理 + +3. **使用開始**(AIアシスタントにこれらのプロンプトを与える): + ``` + プロジェクトパスを/Users/dev/my-react-appに設定 + このプロジェクトのすべてのTypeScriptファイルを検索 + 「authentication」関連関数を検索 + メインのApp.tsxファイルを解析 + ``` + *起動時に `--project-path` を付けた場合は、最初のコマンドは不要です。サーバーが既にパスを認識しています。* + +### Codex CLI 設定 + +Anthropic の Codex CLI を使用している場合は、`~/.codex/config.toml` に次のサーバー設定を追加します。 +Windows では `C:\Users\\.codex\config.toml` に保存されています。 + +```toml +[mcp_servers.code-index] +type = "stdio" +command = "uvx" +args = ["code-index-mcp"] +``` +> 起動時にプロジェクトを設定したい場合は、`args` リストに `--project-path C:/absolute/path/to/repo` を追加してください。 +> これは起動後に `set_project_path` ツールを呼び出すのと同じ効果です。 + +Windows の `uvx` は標準ユーザープロファイルディレクトリが必要です。 +MCP を安定して起動するために、同じブロックに次の環境変数を残してください。 + +```toml +env = { + HOME = "C:\\Users\\", + APPDATA = "C:\\Users\\\\AppData\\Roaming", + LOCALAPPDATA = "C:\\Users\\\\AppData\\Local", + SystemRoot = "C:\\Windows" +} +``` + +Linux と macOS では OS が `HOME` や XDG 系のパスを標準で公開しているため、通常は `env` セクションは不要です。 +制限されたコンテナで実行する場合やキャッシュ/設定の保存先を手動で変更したいときだけ上書きしてください。 +環境変数の一覧は [`uv` の環境変数リファレンス](https://docs.astral.sh/uv/reference/environment/)(`HOME`、`XDG_CACHE_HOME`、`XDG_CONFIG_HOME`、`APPDATA` など)を参照してください。 + + +## 一般的な使用ケース + +**コードレビュー**:「旧いAPIを使用しているすべての箇所を検索」 +**リファクタリング支援**:「この関数はどこで呼ばれている?」 +**プロジェクト学習**:「このReactプロジェクトの主要コンポーネントを表示」 +**デバッグ支援**:「エラーハンドリング関連のコードをすべて検索」 + +## 主な機能 + +### 🔍 **インテリジェント検索・解析** +- **二重戦略アーキテクチャ**:7つのコア言語に特化したTree-sitter解析、50+ファイルタイプにフォールバック戦略 +- **直接Tree-sitter統合**:特化言語で正規表現フォールバックなし - 明確なエラーメッセージで高速フェイル +- **高度な検索**:最適なツール(ugrep、ripgrep、ag、grep)を自動検出・使用 +- **汎用ファイルサポート**:高度なAST解析から基本ファイルインデックスまでの包括的カバレッジ +- **ファイル解析**:`build_deep_index` 実行後に構造、インポート、クラス、メソッド、複雑度メトリクスを深く把握 + +### 🗂️ **多言語サポート** +- **7言語でTree-sitter AST解析**:Python、JavaScript、TypeScript、Java、Go、Objective-C、Zig +- **50+ファイルタイプでフォールバック戦略**:C/C++、Rust、Ruby、PHPおよびすべての他のプログラミング言語 +- **文書・設定ファイル**:Markdown、JSON、YAML、XML適切な処理 +- **Webフロントエンド**:Vue、React、Svelte、HTML、CSS、SCSS +- **Java Webとビルド**:JSP/タグファイル(`.jsp`, `.jspx`, `.jspf`, `.tag`, `.tagx`)、Grails/GSP(`.gsp`)、Gradle/Groovyスクリプト(`.gradle`, `.groovy`)、`.properties`、Protocol Buffers(`.proto`) +- **データベース**:SQLバリアント、NoSQL、ストアドプロシージャ、マイグレーション +- **設定ファイル**:JSON、YAML、XML、Markdown +- **[完全なリストを表示](#サポートされているファイルタイプ)** + +### ⚡ **リアルタイム監視・自動更新** +- **ファイルウォッチャー**:ファイル変更時の自動インデックス更新 +- **クロスプラットフォーム**:ネイティブOSファイルシステム監視 +- **スマート処理**:急速な変更をバッチ処理して過度な再構築を防止 +- **浅いインデックス更新**:ファイル変更を監視して最新のファイル一覧を維持し、シンボルが必要な場合は `build_deep_index` を実行 + +### ⚡ **パフォーマンス・効率性** +- **Tree-sitter AST解析**:正確なシンボル抽出のためのネイティブ構文解析 +- **永続キャッシュ**:超高速な後続アクセスのためのインデックス保存 +- **スマートフィルタリング**:ビルドディレクトリと一時ファイルのインテリジェント除外 +- **メモリ効率**:大規模コードベース向けに最適化 +- **直接依存関係**:フォールバック機構なし - 明確なエラーメッセージで高速フェイル + +## サポートされているファイルタイプ + +
+📁 プログラミング言語(クリックで展開) + +**特化Tree-sitter戦略言語:** +- **Python** (`.py`, `.pyw`) - クラス/メソッド抽出と呼び出し追跡を含む完全AST解析 +- **JavaScript** (`.js`, `.jsx`, `.mjs`, `.cjs`) - Tree-sitterを使用したES6+クラスと関数解析 +- **TypeScript** (`.ts`, `.tsx`) - インターフェースを含む完全な型認識シンボル抽出 +- **Java** (`.java`) - 完全なクラス階層、メソッドシグネチャ、呼び出し関係 +- **Go** (`.go`) - 構造体メソッド、レシーバータイプ、関数解析 +- **Objective-C** (`.m`, `.mm`) - +/-記法を使用したクラス/インスタンスメソッド区別 +- **Zig** (`.zig`, `.zon`) - Tree-sitter ASTを使用した関数と構造体解析 + +**すべての他のプログラミング言語:** +すべての他のプログラミング言語は**フォールバック解析戦略**を使用し、基本ファイルインデックスとメタデータ抽出を提供します。これには以下が含まれます: +- **システム・低レベル言語:** C/C++ (`.c`, `.cpp`, `.h`, `.hpp`)、Rust (`.rs`) +- **オブジェクト指向言語:** C# (`.cs`)、Kotlin (`.kt`)、Scala (`.scala`)、Swift (`.swift`) +- **スクリプト・動的言語:** Ruby (`.rb`)、PHP (`.php`)、Shell (`.sh`, `.bash`) +- **および40+ファイルタイプ** - すべてフォールバック戦略による基本インデックス処理 + +
+ +
+🌐 Web・フロントエンド(クリックで展開) + +**フレームワーク・ライブラリ:** +- Vue (`.vue`) +- Svelte (`.svelte`) +- Astro (`.astro`) + +**スタイリング:** +- CSS (`.css`, `.scss`, `.less`, `.sass`, `.stylus`, `.styl`) +- HTML (`.html`) + +**テンプレート:** +- Handlebars (`.hbs`, `.handlebars`) +- EJS (`.ejs`) +- Pug (`.pug`) +- FreeMarker (`.ftl`) +- Mustache (`.mustache`) +- Liquid (`.liquid`) +- ERB (`.erb`) + +
+ +
+🗄️ データベース・SQL(クリックで展開) + +**SQL バリアント:** +- 標準SQL (`.sql`, `.ddl`, `.dml`) +- データベース固有 (`.mysql`, `.postgresql`, `.psql`, `.sqlite`, `.mssql`, `.oracle`, `.ora`, `.db2`) + +**データベースオブジェクト:** +- プロシージャ・関数 (`.proc`, `.procedure`, `.func`, `.function`) +- ビュー・トリガー (`.view`, `.trigger`, `.index`) + +**マイグレーション・ツール:** +- マイグレーションファイル (`.migration`, `.seed`, `.fixture`, `.schema`) +- ツール固有 (`.liquibase`, `.flyway`) + +**NoSQL・モダンDB:** +- グラフ・クエリ (`.cql`, `.cypher`, `.sparql`, `.gql`) + +
+ +
+📄 ドキュメント・設定(クリックで展開) + +- Markdown (`.md`, `.mdx`) +- 設定 (`.json`, `.xml`, `.yml`, `.yaml`, `.properties`) + +
+ +## クイックスタート + +### 🚀 **推奨セットアップ(ほとんどのユーザー向け)** + +任意のMCP対応アプリケーションで開始する最も簡単な方法: + +**前提条件:** Python 3.10+ と [uv](https://github.com/astral-sh/uv) + +1. **MCP設定に追加**(例:`claude_desktop_config.json` または `~/.claude.json`): + ```json + { + "mcpServers": { + "code-index": { + "command": "uvx", + "args": ["code-index-mcp"] + } + } + } + ``` + +2. **アプリケーションを再起動** – `uvx` が自動的にインストールと実行を処理 + +### 🛠️ **開発セットアップ** + +貢献やローカル開発用: + +1. **クローンとインストール:** + ```bash + git clone https://github.com/johnhuang316/code-index-mcp.git + cd code-index-mcp + uv sync + ``` + +2. **ローカル開発用設定:** + ```json + { + "mcpServers": { + "code-index": { + "command": "uv", + "args": ["run", "code-index-mcp"] + } + } + } + ``` + +3. **MCP Inspectorでデバッグ:** + ```bash + npx @modelcontextprotocol/inspector uv run code-index-mcp + ``` + +
+代替案:手動pipインストール + +従来のpip管理を好む場合: + +```bash +pip install code-index-mcp +``` + +そして設定: +```json +{ + "mcpServers": { + "code-index": { + "command": "code-index-mcp", + "args": [] + } + } +} +``` + +
+ + +## 利用可能なツール + +### 🏗️ **プロジェクト管理** +| ツール | 説明 | +|--------|------| +| **`set_project_path`** | プロジェクトディレクトリのインデックス作成を初期化 | +| **`refresh_index`** | ファイル変更後に浅いファイルインデックスを再構築 | +| **`build_deep_index`** | 深い解析で使う完全なシンボルインデックスを生成 | +| **`get_settings_info`** | 現在のプロジェクト設定と状態を表示 | + +*シンボルレベルのデータが必要な場合は `build_deep_index` を実行してください。デフォルトの浅いインデックスは高速なファイル探索を担います。* + +### 🔍 **検索・発見** +| ツール | 説明 | +|--------|------| +| **`search_code_advanced`** | 正規表現、ファジーマッチング、ファイルフィルタリング対応のスマート検索。デフォルトで 1 ページあたり 10 件を返し、`max_results` と `start_index` で調整可能 | +| **`find_files`** | globパターンを使用したファイル検索(例:`**/*.py`) | +| **`get_file_summary`** | ファイル構造、関数、インポート、複雑度の解析(深いインデックスが必要) | + +### 🔄 **監視・自動更新** +| ツール | 説明 | +|--------|------| +| **`get_file_watcher_status`** | ファイルウォッチャーの状態と設定を確認 | +| **`configure_file_watcher`** | 自動更新の有効化/無効化と設定の構成 | + +### 🛠️ **システム・メンテナンス** +| ツール | 説明 | +|--------|------| +| **`create_temp_directory`** | インデックスデータの保存ディレクトリをセットアップ | +| **`check_temp_directory`** | インデックス保存場所と権限を確認 | +| **`clear_settings`** | すべてのキャッシュデータと設定をリセット | +| **`refresh_search_tools`** | 利用可能な検索ツール(ugrep、ripgrep等)を再検出 | + +## 使用例 + +### 🎯 **クイックスタートワークフロー** + +**1. プロジェクトの初期化** +``` +プロジェクトパスを /Users/dev/my-react-app に設定してください +``` +*コードベースを自動インデックス作成し、検索可能なキャッシュを構築* + +**2. プロジェクト構造の探索** +``` +src/components で全てのTypeScriptコンポーネントファイルを見つけてください +``` +*使用ツール:`find_files`、パターン `src/components/**/*.tsx`* + +**3. キーファイルの解析** +``` +src/api/userService.ts の要約を教えてください +``` +*使用ツール:`get_file_summary` で関数、インポート、複雑度を表示* +*ヒント:`needs_deep_index` が返った場合は `build_deep_index` を先に実行してください。* + +### 🔍 **高度な検索例** + +
+コードパターン検索 + +``` +正規表現を使って "get.*Data" にマッチする全ての関数呼び出しを検索してください +``` +*発見:`getData()`、`getUserData()`、`getFormData()` など* + +
+ +
+ファジー関数検索 + +``` +'authUser' でファジー検索して認証関連の関数を見つけてください +``` +*マッチ:`authenticateUser`、`authUserToken`、`userAuthCheck` など* + +
+ +
+言語固有検索 + +``` +Pythonファイルのみで "API_ENDPOINT" を検索してください +``` +*使用ツール:`search_code_advanced`、`file_pattern: "*.py"`(デフォルトは 10 件。`max_results` で件数を増やし、`start_index` でページ送り)* + +
+ +
+自動更新設定 + +``` +ファイル変更時の自動インデックス更新を設定してください +``` +*使用ツール:`configure_file_watcher` で監視の有効化/無効化とデバウンス時間を設定* + +
+ +
+プロジェクトメンテナンス + +``` +新しいコンポーネントを追加したので、プロジェクトインデックスを更新してください +``` +*使用ツール:`refresh_index` で検索可能なキャッシュを更新* + +
+ +## トラブルシューティング + +### 🔄 **自動リフレッシュが動作しない** + +ファイル変更時に自動インデックス更新が動作しない場合、以下を試してください: +- `pip install watchdog`(環境分離の問題を解決する可能性があります) +- 手動リフレッシュを使用:ファイル変更後に `refresh_index` ツールを呼び出す +- ファイルウォッチャーステータスを確認:`get_file_watcher_status` を使用して監視がアクティブかどうかを確認 + +## 開発・貢献 + +### 🔧 **ソースからのビルド** +```bash +git clone https://github.com/johnhuang316/code-index-mcp.git +cd code-index-mcp +uv sync +uv run code-index-mcp +``` + +### 🐛 **デバッグ** +```bash +npx @modelcontextprotocol/inspector uvx code-index-mcp +``` + +### 🤝 **貢献** +貢献を歓迎します!お気軽にプルリクエストを提出してください。 + +--- + +### 📜 **ライセンス** +[MIT License](LICENSE) + +### 🌐 **翻訳** +- [English](README.md) +- [繁體中文](README_zh.md) diff --git a/reference/code-index-mcp-master/README_ko.md b/reference/code-index-mcp-master/README_ko.md new file mode 100644 index 00000000..000898d0 --- /dev/null +++ b/reference/code-index-mcp-master/README_ko.md @@ -0,0 +1,319 @@ +# 코드 인덱스 MCP + +
+ +[![MCP Server](https://img.shields.io/badge/MCP-Server-blue)](https://modelcontextprotocol.io) +[![Python](https://img.shields.io/badge/Python-3.10%2B-green)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE) + +**대규모 언어 모델을 위한 지능형 코드 인덱싱과 분석** + +고급 검색, 정밀 분석, 유연한 탐색 기능으로 AI가 코드베이스를 이해하고 활용하는 방식을 혁신하세요. + +
+ + + code-index-mcp MCP server + + +## 개요 + +Code Index MCP는 [Model Context Protocol](https://modelcontextprotocol.io) 기반 MCP 서버로, AI 어시스턴트와 복잡한 코드베이스 사이를 연결합니다. 빠른 인덱싱, 강력한 검색, 정밀한 코드 분석을 제공하여 AI가 프로젝트 구조를 정확히 파악하고 효과적으로 지원하도록 돕습니다. + +**이럴 때 안성맞춤:** 코드 리뷰, 리팩터링, 문서화, 디버깅 지원, 아키텍처 분석 + +## 빠른 시작 + +### 🚀 **권장 설정 (대부분의 사용자)** + +어떤 MCP 호환 애플리케이션에서도 몇 단계만으로 시작할 수 있습니다. + +**사전 준비:** Python 3.10+ 및 [uv](https://github.com/astral-sh/uv) + +1. **MCP 설정에 서버 추가** (예: `claude_desktop_config.json` 또는 `~/.claude.json`) + ```json + { + "mcpServers": { + "code-index": { + "command": "uvx", + "args": ["code-index-mcp"] + } + } + } + ``` + + > 시작할 때 프로젝트를 자동으로 지정하려면 `args` 배열 끝에 + > `--project-path /absolute/path/to/repo` 를 추가하세요. 이렇게 하면 시작 직후 `set_project_path` 를 호출한 것과 동일한 상태가 됩니다. +2. **애플리케이션 재시작** – `uvx`가 설치와 실행을 자동으로 처리합니다. + +3. **사용 시작** (AI 어시스턴트에게 아래 프롬프트를 전달) + ``` + 프로젝트 경로를 /Users/dev/my-react-app 으로 설정해줘 + 이 프로젝트에서 모든 TypeScript 파일을 찾아줘 + "authentication" 관련 함수를 검색해줘 + src/App.tsx 파일을 분석해줘 + ``` + *실행 시 `--project-path` 옵션을 사용했다면 첫 번째 명령은 건너뛰어도 됩니다. 서버가 이미 경로를 알고 있습니다.* + +### Codex CLI 설정 + +Anthropic의 Codex CLI를 사용하는 경우 `~/.codex/config.toml`에 다음 MCP 서버 설정을 추가하세요. +Windows에서는 `C:\Users\\.codex\config.toml`에 위치합니다. + +```toml +[mcp_servers.code-index] +type = "stdio" +command = "uvx" +args = ["code-index-mcp"] +``` +> 실행 시 프로젝트를 자동으로 지정하려면 `args` 리스트에 `--project-path C:/absolute/path/to/repo` 를 추가하세요. +> 이는 이후에 `set_project_path` 도구를 호출하는 것과 같은 효과입니다. + +Windows의 `uvx`는 기본 사용자 프로필 디렉터리가 필요합니다. +MCP가 안정적으로 시작되도록 같은 블록에 아래 환경 변수 덮어쓰기를 유지하세요. + +```toml +env = { + HOME = "C:\Users\", + APPDATA = "C:\Users\\AppData\Roaming", + LOCALAPPDATA = "C:\Users\\AppData\Local", + SystemRoot = "C:\Windows" +} +``` + +Linux와 macOS는 운영체제가 `HOME`과 XDG 경로를 기본으로 제공하므로 대부분 별도의 `env` 섹션이 필요하지 않습니다. +제한된 컨테이너에서 실행하거나 캐시/설정 위치를 수동으로 바꾸고 싶을 때만 덮어쓰면 됩니다. +환경 변수 전체 목록은 [`uv` 환경 변수 문서](https://docs.astral.sh/uv/reference/environment/) (`HOME`, `XDG_CACHE_HOME`, `XDG_CONFIG_HOME`, `APPDATA` 등)를 참고하세요. + + +## 대표 사용 사례 + +**코드 리뷰:** "예전 API를 사용하는 부분을 모두 찾아줘" +**리팩터링 지원:** "이 함수는 어디에서 호출되나요?" +**프로젝트 학습:** "이 React 프로젝트의 핵심 컴포넌트를 보여줘" +**디버깅:** "에러 처리 로직이 있는 파일을 찾아줘" + +## 주요 기능 + +### 🧠 **지능형 검색과 분석** +- **듀얼 전략 아키텍처:** 7개 핵심 언어는 전용 tree-sitter 파서를 사용하고, 그 외 50+ 파일 형식은 폴백 전략으로 처리 +- **직접 Tree-sitter 통합:** 특화 언어에 정규식 폴백 없음 – 문제 시 즉시 실패하고 명확한 오류 메시지 제공 +- **고급 검색:** ugrep, ripgrep, ag, grep 중 최적의 도구를 자동 선택해 활용 +- **범용 파일 지원:** 정교한 AST 분석부터 기본 파일 인덱싱까지 폭넓게 커버 +- **파일 분석:** `build_deep_index` 실행 후 구조, 임포트, 클래스, 메서드, 복잡도 지표를 심층적으로 파악 + +### 🗂️ **다중 언어 지원** +- **Tree-sitter AST 분석(7종):** Python, JavaScript, TypeScript, Java, Go, Objective-C, Zig +- **폴백 전략(50+ 형식):** C/C++, Rust, Ruby, PHP 등 대부분의 프로그래밍 언어 지원 +- **문서 및 설정 파일:** Markdown, JSON, YAML, XML 등 상황에 맞는 처리 +- **웹 프론트엔드:** Vue, React, Svelte, HTML, CSS, SCSS +- **Java 웹 & 빌드:** JSP/태그 (`.jsp`, `.jspx`, `.jspf`, `.tag`, `.tagx`), Grails/GSP (`.gsp`), Gradle/Groovy 스크립트 (`.gradle`, `.groovy`), `.properties`, Protocol Buffers (`.proto`) +- **데이터 계층:** SQL, NoSQL, 스토어드 프로시저, 마이그레이션 스크립트 +- **구성 파일:** JSON, YAML, XML, Markdown +- **[지원 파일 전체 목록 보기](#지원-파일-형식)** + +### 🔄 **실시간 모니터링 & 자동 새로고침** +- **파일 워처:** 파일 변경 시 자동으로 얕은 인덱스(파일 목록) 갱신 +- **크로스 플랫폼:** 운영체제 기본 파일시스템 이벤트 활용 +- **스마트 처리:** 빠른 변경을 묶어 과도한 재빌드를 방지 +- **얕은 인덱스 갱신:** 파일 목록을 최신 상태로 유지하며, 심볼 데이터가 필요하면 `build_deep_index`를 실행 + +### ⚡ **성능 & 효율성** +- **Tree-sitter AST 파싱:** 정확한 심볼 추출을 위한 네이티브 구문 분석 +- **지속 캐싱:** 인덱스를 저장해 이후 응답 속도를 극대화 +- **스마트 필터링:** 빌드 디렉터리·임시 파일을 자동 제외 +- **메모리 효율:** 대규모 코드베이스를 염두에 둔 설계 +- **직접 의존성:** 불필요한 폴백 없이 명확한 오류 메시지 제공 + +## 지원 파일 형식 + +
+💻 프로그래밍 언어 (클릭하여 확장) + +**전용 Tree-sitter 전략 언어:** +- **Python** (`.py`, `.pyw`) – 클래스/메서드 추출 및 호출 추적이 포함된 완전 AST 분석 +- **JavaScript** (`.js`, `.jsx`, `.mjs`, `.cjs`) – ES6+ 클래스와 함수를 tree-sitter로 파싱 +- **TypeScript** (`.ts`, `.tsx`) – 인터페이스를 포함한 타입 인지 심볼 추출 +- **Java** (`.java`) – 클래스 계층, 메서드 시그니처, 호출 관계 분석 +- **Go** (`.go`) – 구조체 메서드, 리시버 타입, 함수 분석 +- **Objective-C** (`.m`, `.mm`) – 클래스/인스턴스 메서드를 +/- 표기로 구분 +- **Zig** (`.zig`, `.zon`) – 함수와 구조체를 tree-sitter AST로 분석 + +**기타 모든 프로그래밍 언어:** +나머지 언어는 **폴백 파싱 전략**으로 기본 메타데이터와 파일 인덱싱을 제공합니다. 예: +- **시스템/저수준:** C/C++ (`.c`, `.cpp`, `.h`, `.hpp`), Rust (`.rs`) +- **객체지향:** C# (`.cs`), Kotlin (`.kt`), Scala (`.scala`), Swift (`.swift`) +- **스크립트:** Ruby (`.rb`), PHP (`.php`), Shell (`.sh`, `.bash`) +- **그 외 40+ 형식** – 폴백 전략으로 빠른 탐색 가능 + +
+ +
+🌐 웹 프론트엔드 & UI + +- 프레임워크: Vue (`.vue`), Svelte (`.svelte`), Astro (`.astro`) +- 스타일링: CSS (`.css`, `.scss`, `.less`, `.sass`, `.stylus`, `.styl`), HTML (`.html`) +- 템플릿: Handlebars (`.hbs`, `.handlebars`), EJS (`.ejs`), Pug (`.pug`), FreeMarker (`.ftl`), Mustache (`.mustache`), Liquid (`.liquid`), ERB (`.erb`) + +
+ +
+🗄️ 데이터 계층 & SQL + +- **SQL 변형:** 표준 SQL (`.sql`, `.ddl`, `.dml`), 데이터베이스별 방언 (`.mysql`, `.postgresql`, `.psql`, `.sqlite`, `.mssql`, `.oracle`, `.ora`, `.db2`) +- **DB 객체:** 프로시저/함수 (`.proc`, `.procedure`, `.func`, `.function`), 뷰/트리거/인덱스 (`.view`, `.trigger`, `.index`) +- **마이그레이션 도구:** 마이그레이션 파일 (`.migration`, `.seed`, `.fixture`, `.schema`), 도구 구성 (`.liquibase`, `.flyway`) +- **NoSQL & 그래프:** 질의 언어 (`.cql`, `.cypher`, `.sparql`, `.gql`) + +
+ +
+📄 문서 & 설정 파일 + +- Markdown (`.md`, `.mdx`) +- 구성 파일 (`.json`, `.xml`, `.yml`, `.yaml`, `.properties`) + +
+ +## 사용 가능한 도구 + +### 🏗️ **프로젝트 관리** +| 도구 | 설명 | +|------|------| +| **`set_project_path`** | 프로젝트 디렉터리의 인덱스를 초기화 | +| **`refresh_index`** | 파일 변경 후 얕은 파일 인덱스를 재생성 | +| **`build_deep_index`** | 심층 분석에 사용하는 전체 심볼 인덱스를 생성 | +| **`get_settings_info`** | 현재 프로젝트 설정과 상태를 확인 | + +*심볼 레벨 데이터가 필요하면 `build_deep_index`를 실행하세요. 기본 얕은 인덱스는 빠른 파일 탐색을 담당합니다.* + +### 🔍 **검색 & 탐색** +| 도구 | 설명 | +|------|------| +| **`search_code_advanced`** | 정규식, 퍼지 매칭, 파일 필터링을 지원하는 스마트 검색 (기본적으로 페이지당 10개 결과 반환, `max_results`·`start_index`로 조정 가능) | +| **`find_files`** | 글롭 패턴으로 파일 찾기 (예: `**/*.py`) | +| **`get_file_summary`** | 파일 구조, 함수, 임포트, 복잡도를 분석 (심층 인덱스 필요) | + +### 🔄 **모니터링 & 자동 새로고침** +| 도구 | 설명 | +|------|------| +| **`get_file_watcher_status`** | 파일 워처 상태와 구성을 확인 | +| **`configure_file_watcher`** | 자동 새로고침 설정 (활성/비활성, 지연 시간, 추가 제외 패턴) | + +### 🛠️ **시스템 & 유지 관리** +| 도구 | 설명 | +|------|------| +| **`create_temp_directory`** | 인덱스 저장용 임시 디렉터리를 생성 | +| **`check_temp_directory`** | 인덱스 저장 위치와 권한을 확인 | +| **`clear_settings`** | 모든 설정과 캐시 데이터를 초기화 | +| **`refresh_search_tools`** | 사용 가능한 검색 도구를 재검색 (ugrep, ripgrep 등) | + +## 사용 예시 + +### 🧭 **빠른 시작 워크플로** + +**1. 프로젝트 초기화** +``` +프로젝트 경로를 /Users/dev/my-react-app 으로 설정해줘 +``` +*프로젝트를 설정하고 얕은 인덱스를 생성합니다.* + +**2. 프로젝트 구조 탐색** +``` +src/components 안의 TypeScript 컴포넌트 파일을 모두 찾아줘 +``` +*사용 도구: `find_files` (`src/components/**/*.tsx`)* + +**3. 핵심 파일 분석** +``` +src/api/userService.ts 요약을 알려줘 +``` +*사용 도구: `get_file_summary` (함수, 임포트, 복잡도 표시)* +*팁: `needs_deep_index` 응답이 나오면 먼저 `build_deep_index`를 실행하세요.* + +### 🔍 **고급 검색 예시** + +
+코드 패턴 검색 + +``` +"get.*Data"에 해당하는 함수 호출을 정규식으로 찾아줘 +``` +*예: `getData()`, `getUserData()`, `getFormData()`* + +
+ +
+퍼지 함수 검색 + +``` +'authUser'와 유사한 인증 관련 함수를 찾아줘 +``` +*예: `authenticateUser`, `authUserToken`, `userAuthCheck`* + +
+ +
+언어별 검색 + +``` +Python 파일에서만 "API_ENDPOINT" 를 찾아줘 +``` +*`search_code_advanced` + `file_pattern="*.py"` (기본 10개 결과, `max_results`로 확장하고 `start_index`로 페이지 이동)* + +
+ +
+자동 새로고침 설정 + +``` +파일 변경 시 자동으로 인덱스를 새로고침하도록 설정해줘 +``` +*`configure_file_watcher`로 활성화 및 지연 시간 설정* + +
+ +
+프로젝트 유지 관리 + +``` +새 컴포넌트를 추가했어. 프로젝트 인덱스를 다시 빌드해줘 +``` +*`refresh_index`로 빠르게 얕은 인덱스를 업데이트* + +
+ +## 문제 해결 + +### 🔄 **자동 새로고침이 동작하지 않을 때** +- 환경 문제로 `watchdog`가 빠졌다면 설치: `pip install watchdog` +- 수동 새로고침: 변경 후 `refresh_index` 도구 실행 +- 워처 상태 확인: `get_file_watcher_status` 도구로 활성 여부 점검 + +## 개발 & 기여 + +### 🛠️ **소스에서 실행하기** +```bash +git clone https://github.com/johnhuang316/code-index-mcp.git +cd code-index-mcp +uv sync +uv run code-index-mcp +``` + +### 🧪 **디버깅 도구** +```bash +npx @modelcontextprotocol/inspector uvx code-index-mcp +``` + +### 🤝 **기여 안내** +Pull Request를 언제든 환영합니다. 변경 사항과 테스트 방법을 함께 공유해주세요. + +--- + +### 📄 **라이선스** +[MIT License](LICENSE) + +### 🌍 **번역본** +- [English](README.md) +- [繁體中文](README_zh.md) +- [日本語](README_ja.md) diff --git a/reference/code-index-mcp-master/README_zh.md b/reference/code-index-mcp-master/README_zh.md new file mode 100644 index 00000000..e8d092ba --- /dev/null +++ b/reference/code-index-mcp-master/README_zh.md @@ -0,0 +1,416 @@ +# 程式碼索引 MCP + +
+ +[![MCP Server](https://img.shields.io/badge/MCP-Server-blue)](https://modelcontextprotocol.io) +[![Python](https://img.shields.io/badge/Python-3.10%2B-green)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE) + +**為大型語言模型提供智慧程式碼索引與分析** + +以先進的搜尋、分析和導航功能,徹底改變 AI 對程式碼庫的理解方式。 + +
+ + + code-index-mcp MCP server + + +## 概述 + +程式碼索引 MCP 是一個 [模型上下文協定](https://modelcontextprotocol.io) 伺服器,架起 AI 模型與複雜程式碼庫之間的橋樑。它提供智慧索引、先進搜尋功能和詳細程式碼分析,幫助 AI 助理有效地理解和導航您的專案。 + +**適用於:**程式碼審查、重構、文件生成、除錯協助和架構分析。 + +## 快速開始 + +### 🚀 **推薦設定(大多數使用者)** + +與任何 MCP 相容應用程式開始的最簡單方式: + +**前置需求:** Python 3.10+ 和 [uv](https://github.com/astral-sh/uv) + +1. **新增到您的 MCP 設定** (例如 `claude_desktop_config.json` 或 `~/.claude.json`): + ```json + { + "mcpServers": { + "code-index": { + "command": "uvx", + "args": ["code-index-mcp"] + } + } + } + ``` + > 若想在啟動時自動設定專案路徑,可在 `args` 陣列末尾加入 + > `--project-path /絕對/路徑`,效果等同於啟動後呼叫 `set_project_path`。 + +2. **重新啟動應用程式** – `uvx` 會自動處理安裝和執行 + +3. **開始使用**(向您的 AI 助理提供這些提示): + ``` + 設定專案路徑為 /Users/dev/my-react-app + 在這個專案中找到所有 TypeScript 檔案 + 搜尋「authentication」相關函數 + 分析主要的 App.tsx 檔案 + ``` + *如果啟動時已提供 `--project-path`,可以略過第一個指令,伺服器會自動記住路徑。* + +### Codex CLI 設定 + +如果你使用 Anthropic 的 Codex CLI,請在 `~/.codex/config.toml` 中加入下列伺服器設定。 +Windows 的設定檔位於 `C:\Users\\.codex\config.toml`: + +```toml +[mcp_servers.code-index] +type = "stdio" +command = "uvx" +args = ["code-index-mcp"] +``` +> 如需要啟動時自動設定專案,請把 `--project-path C:/絕對/路徑` 加到 `args` +> 清單中,與手動呼叫 `set_project_path` 的結果相同。 + +在 Windows 中,`uvx` 需要找到標準的使用者目錄。保留下列環境變數覆寫設定,才能讓 MCP 穩定啟動: + +```toml +env = { + HOME = "C:\\Users\\", + APPDATA = "C:\\Users\\\\AppData\\Roaming", + LOCALAPPDATA = "C:\\Users\\\\AppData\\Local", + SystemRoot = "C:\\Windows" +} +``` + +Linux 與 macOS 預設會提供 `HOME` 與 XDG 路徑,因此通常不需要額外的 `env` 區塊;只有在受限的容器環境或想手動調整快取/設定位置時才需要覆寫。 +完整的環境變數清單請參考 [`uv` 環境變數說明](https://docs.astral.sh/uv/reference/environment/)(包含 `HOME`、`XDG_CACHE_HOME`、`XDG_CONFIG_HOME`、`APPDATA` 等)。 + + +## 典型使用場景 + +**程式碼審查**:「找出所有使用舊 API 的地方」 +**重構協助**:「這個函數在哪裡被呼叫?」 +**學習專案**:「顯示這個 React 專案的主要元件」 +**除錯協助**:「搜尋所有錯誤處理相關的程式碼」 + +## 主要特性 + +### 🔍 **智慧搜尋與分析** +- **雙策略架構**:7 種核心語言使用專業化 Tree-sitter 解析,50+ 種檔案類型使用備用策略 +- **直接 Tree-sitter 整合**:專業化語言無正則表達式備用 - 快速失敗並提供清晰錯誤訊息 +- **進階搜尋**:自動偵測並使用最佳工具(ugrep、ripgrep、ag 或 grep) +- **通用檔案支援**:從進階 AST 解析到基本檔案索引的全面覆蓋 +- **檔案分析**:執行 `build_deep_index` 後深入了解結構、匯入、類別、方法和複雜度指標 + +### 🗂️ **多語言支援** +- **7 種語言使用 Tree-sitter AST 解析**:Python、JavaScript、TypeScript、Java、Go、Objective-C、Zig +- **50+ 種檔案類型使用備用策略**:C/C++、Rust、Ruby、PHP 和所有其他程式語言 +- **文件與配置檔案**:Markdown、JSON、YAML、XML 適當處理 +- **網頁前端**:Vue、React、Svelte、HTML、CSS、SCSS +- **Java Web 與建置**:JSP/Tag (`.jsp`, `.jspx`, `.jspf`, `.tag`, `.tagx`)、Grails/GSP (`.gsp`)、Gradle/Groovy 腳本 (`.gradle`, `.groovy`)、`.properties`、Protocol Buffers (`.proto`) +- **資料庫**:SQL 變體、NoSQL、存儲過程、遷移腳本 +- **配置檔案**:JSON、YAML、XML、Markdown +- **[查看完整列表](#支援的檔案類型)** + +### ⚡ **即時監控與自動刷新** +- **檔案監控器**:檔案變更時自動更新索引 +- **跨平台**:原生作業系統檔案系統監控 +- **智慧處理**:批次處理快速變更以防止過度重建 +- **淺層索引更新**:監控檔案變更並維持檔案清單最新;需要符號資料時請執行 `build_deep_index` + +### ⚡ **效能與效率** +- **Tree-sitter AST 解析**:原生語法解析以實現準確的符號提取 +- **持久快取**:儲存索引以實現超快速的後續存取 +- **智慧篩選**:智能排除建構目錄和暫存檔案 +- **記憶體高效**:針對大型程式碼庫優化 +- **直接依賴**:無備用機制 - 快速失敗並提供清晰錯誤訊息 + +## 支援的檔案類型 + +
+📁 程式語言(點擊展開) + +**專業化 Tree-sitter 策略語言:** +- **Python** (`.py`, `.pyw`) - 完整 AST 分析,包含類別/方法提取和呼叫追蹤 +- **JavaScript** (`.js`, `.jsx`, `.mjs`, `.cjs`) - ES6+ 類別和函數解析使用 Tree-sitter +- **TypeScript** (`.ts`, `.tsx`) - 完整類型感知符號提取,包含介面 +- **Java** (`.java`) - 完整類別階層、方法簽名和呼叫關係 +- **Go** (`.go`) - 結構方法、接收者類型和函數分析 +- **Objective-C** (`.m`, `.mm`) - 類別/實例方法區分,使用 +/- 標記法 +- **Zig** (`.zig`, `.zon`) - 函數和結構解析使用 Tree-sitter AST + +**所有其他程式語言:** +所有其他程式語言使用 **備用解析策略**,提供基本檔案索引和元資料提取。包括: +- **系統與低階語言:** C/C++ (`.c`, `.cpp`, `.h`, `.hpp`)、Rust (`.rs`) +- **物件導向語言:** C# (`.cs`)、Kotlin (`.kt`)、Scala (`.scala`)、Swift (`.swift`) +- **腳本與動態語言:** Ruby (`.rb`)、PHP (`.php`)、Shell (`.sh`, `.bash`) +- **以及 40+ 種檔案類型** - 全部通過備用策略處理進行基本索引 + +
+ +
+🌐 網頁與前端(點擊展開) + +**框架與函式庫:** +- Vue (`.vue`) +- Svelte (`.svelte`) +- Astro (`.astro`) + +**樣式:** +- CSS (`.css`, `.scss`, `.less`, `.sass`, `.stylus`, `.styl`) +- HTML (`.html`) + +**模板:** +- Handlebars (`.hbs`, `.handlebars`) +- EJS (`.ejs`) +- Pug (`.pug`) +- FreeMarker (`.ftl`) +- Mustache (`.mustache`) +- Liquid (`.liquid`) +- ERB (`.erb`) + +
+ +
+🗄️ 資料庫與 SQL(點擊展開) + +**SQL 變體:** +- 標準 SQL (`.sql`, `.ddl`, `.dml`) +- 資料庫特定 (`.mysql`, `.postgresql`, `.psql`, `.sqlite`, `.mssql`, `.oracle`, `.ora`, `.db2`) + +**資料庫物件:** +- 程序與函式 (`.proc`, `.procedure`, `.func`, `.function`) +- 檢視與觸發器 (`.view`, `.trigger`, `.index`) + +**遷移與工具:** +- 遷移檔案 (`.migration`, `.seed`, `.fixture`, `.schema`) +- 工具特定 (`.liquibase`, `.flyway`) + +**NoSQL 與現代資料庫:** +- 圖形與查詢 (`.cql`, `.cypher`, `.sparql`, `.gql`) + +
+ +
+📄 文件與配置(點擊展開) + +- Markdown (`.md`, `.mdx`) +- 配置 (`.json`, `.xml`, `.yml`, `.yaml`, `.properties`) + +
+ +## 快速開始 + +### 🚀 **建議設定(適用於大多數使用者)** + +在任何相容 MCP 的應用程式中開始使用的最簡單方法: + +**先決條件:** Python 3.10+ 和 [uv](https://github.com/astral-sh/uv) + +1. **新增到您的 MCP 配置**(例如 `claude_desktop_config.json` 或 `~/.claude.json`): + ```json + { + "mcpServers": { + "code-index": { + "command": "uvx", + "args": ["code-index-mcp"] + } + } + } + ``` + +2. **重新啟動您的應用程式** – `uvx` 會自動處理安裝和執行 + +### 🛠️ **開發設定** + +適用於貢獻或本地開發: + +1. **克隆並安裝:** + ```bash + git clone https://github.com/johnhuang316/code-index-mcp.git + cd code-index-mcp + uv sync + ``` + +2. **配置本地開發:** + ```json + { + "mcpServers": { + "code-index": { + "command": "uv", + "args": ["run", "code-index-mcp"] + } + } + } + ``` + +3. **使用 MCP Inspector 除錯:** + ```bash + npx @modelcontextprotocol/inspector uv run code-index-mcp + ``` + +
+替代方案:手動 pip 安裝 + +如果您偏好傳統的 pip 管理: + +```bash +pip install code-index-mcp +``` + +然後配置: +```json +{ + "mcpServers": { + "code-index": { + "command": "code-index-mcp", + "args": [] + } + } +} +``` + +
+ + +## 可用工具 + +### 🏗️ **專案管理** +| 工具 | 描述 | +|------|------| +| **`set_project_path`** | 為專案目錄初始化索引 | +| **`refresh_index`** | 在檔案變更後重建淺層檔案索引 | +| **`build_deep_index`** | 產生供深度分析使用的完整符號索引 | +| **`get_settings_info`** | 檢視目前專案配置和狀態 | + +*需要符號層級資料時,請執行 `build_deep_index`;預設的淺層索引提供快速檔案探索。* + +### 🔍 **搜尋與探索** +| 工具 | 描述 | +|------|------| +| **`search_code_advanced`** | 智慧搜尋,支援正規表達式、模糊匹配和檔案篩選,預設每頁回傳 10 筆結果,可透過 `max_results` 與 `start_index` 調整 | +| **`find_files`** | 使用萬用字元模式尋找檔案(例如 `**/*.py`) | +| **`get_file_summary`** | 分析檔案結構、函式、匯入和複雜度(需要深度索引) | + +### 🔄 **監控與自動刷新** +| 工具 | 描述 | +|------|------| +| **`get_file_watcher_status`** | 檢查檔案監控器狀態和配置 | +| **`configure_file_watcher`** | 啟用/停用自動刷新並配置設定 | + +### 🛠️ **系統與維護** +| 工具 | 描述 | +|------|------| +| **`create_temp_directory`** | 設定索引資料的儲存目錄 | +| **`check_temp_directory`** | 驗證索引儲存位置和權限 | +| **`clear_settings`** | 重設所有快取資料和配置 | +| **`refresh_search_tools`** | 重新偵測可用的搜尋工具(ugrep、ripgrep 等) | + +## 使用範例 + +### 🎯 **快速開始工作流程** + +**1. 初始化您的專案** +``` +將專案路徑設定為 /Users/dev/my-react-app +``` +*自動索引您的程式碼庫並建立可搜尋的快取* + +**2. 探索專案結構** +``` +在 src/components 中尋找所有 TypeScript 元件檔案 +``` +*使用:`find_files`,模式為 `src/components/**/*.tsx`* + +**3. 分析關鍵檔案** +``` +給我 src/api/userService.ts 的摘要 +``` +*使用:`get_file_summary` 顯示函式、匯入和複雜度* +*提示:若收到 `needs_deep_index` 回應,請先執行 `build_deep_index`。* + +### 🔍 **進階搜尋範例** + +
+程式碼模式搜尋 + +``` +使用正規表達式搜尋所有符合 "get.*Data" 的函式呼叫 +``` +*找到:`getData()`、`getUserData()`、`getFormData()` 等* + +
+ +
+模糊函式搜尋 + +``` +使用 'authUser' 的模糊搜尋尋找驗證相關函式 +``` +*匹配:`authenticateUser`、`authUserToken`、`userAuthCheck` 等* + +
+ +
+特定語言搜尋 + +``` +只在 Python 檔案中搜尋 "API_ENDPOINT" +``` +*使用:`search_code_advanced`,`file_pattern: "*.py"`(預設回傳 10 筆;使用 `max_results` 擴充或 `start_index` 換頁)* + +
+ +
+自動刷新配置 + +``` +配置檔案變更時的自動索引更新 +``` +*使用:`configure_file_watcher` 啟用/停用監控並設定防抖時間* + +
+ +
+專案維護 + +``` +我新增了新元件,請重新整理專案索引 +``` +*使用:`refresh_index` 更新可搜尋的快取* + +
+ +## 故障排除 + +### 🔄 **自動刷新無法運作** + +如果檔案變更時自動索引更新無法運作,請嘗試: +- `pip install watchdog`(可能解決環境隔離問題) +- 使用手動刷新:在檔案變更後呼叫 `refresh_index` 工具 +- 檢查檔案監視器狀態:使用 `get_file_watcher_status` 驗證監控是否處於活動狀態 + +## 開發與貢獻 + +### 🔧 **從原始碼建構** +```bash +git clone https://github.com/johnhuang316/code-index-mcp.git +cd code-index-mcp +uv sync +uv run code-index-mcp +``` + +### 🐛 **除錯** +```bash +npx @modelcontextprotocol/inspector uvx code-index-mcp +``` + +### 🤝 **貢獻** +歡迎貢獻!請隨時提交拉取請求。 + +--- + +### 📜 **授權條款** +[MIT 授權條款](LICENSE) + +### 🌐 **翻譯** +- [English](README.md) +- [日本語](README_ja.md) diff --git a/reference/code-index-mcp-master/docs/mcp-restart-playbook.md b/reference/code-index-mcp-master/docs/mcp-restart-playbook.md new file mode 100644 index 00000000..c560c9a0 --- /dev/null +++ b/reference/code-index-mcp-master/docs/mcp-restart-playbook.md @@ -0,0 +1,83 @@ +# MCP Restart Playbook (November 10, 2025) + +This runbook is for the first LLM/agent session *after* the MCP server restarts (for example, after bumping dependencies or recycling the FastMCP process). Follow every step in order so we quickly regain context, validate the upgraded toolchain, and communicate status to the rest of the team. + +--- + +## 1. Current Snapshot +- **Branch**: `mcp-upgrade-notes` +- **Python**: 3.13.2 (uv-managed) +- **Key dependency**: `mcp>=1.21.0,<2.0.0` (synced across `pyproject.toml`, `requirements.txt`, and `uv.lock`) +- **Latest validation**: `uv run pytest` — 16 tests passed on **November 10, 2025 @ 02:05 UTC** +- **Reference doc**: `docs/mcp-upgrade-notes.md` (rationale, API deltas, validation checklist) + +If any of these details drift (new branch, newer SDK, etc.) update this file before handing off. + +--- + +## 2. Post-Restart MCP Calls (must run all tools) +Run through every exposed MCP primitive to guarantee parity after restart. Use the table below as a checklist and record each response summary. + +| # | Tool | Minimum Input | Expected outcome | +|---|------|---------------|------------------| +| 1 | `set_project_path` | `path="C:\Users\p10362321\project\code-index-mcp"` | Indexed ~149 files; watcher initialized. | +| 2 | `build_deep_index` | - | Project re-indexed. Found ~149 files / ~1,070 symbols. | +| 3 | `search_code_advanced` | `pattern="FastMCP", file_pattern="src/**/*.py", max_results=20` | Hits in `server.py` plus pagination metadata. | +| 4 | `find_files` | `pattern="tests/**/*.py"` | Returns 10 test modules. | +| 5 | `get_file_summary` | `file_path="src/code_index_mcp/server.py"` | ~390 lines, 20+ functions reported. | +| 6 | `refresh_index` | - | Shallow index re-built with ~149 files. | +| 7 | `get_settings_info` | - | Shows temp/settings dirs, writable=true. | +| 8 | `create_temp_directory` | - | Confirms directory exists/created. | +| 9 | `check_temp_directory` | - | Lists `index.db`, `index.msgpack`, `index.shallow.json`. | +|10 | `clear_settings` | - | Project settings, index, and cache have been cleared (rerun #1 + #2). | +|11 | `refresh_search_tools` | - | Available: ['ripgrep', 'basic']; preferred: ripgrep. | +|12 | `get_file_watcher_status` | - | status: active, debounce_seconds=6. | +|13 | `configure_file_watcher` | `enabled=True, debounce_seconds=6` | Confirmation message (restart may be required). | + +Notes: +- After running `clear_settings`, immediately repeat `set_project_path` + `build_deep_index` to restore context before proceeding. +- If any tool fails, stop the playbook, capture output, and escalate before continuing. + +Log each response summary in the session notes so the next engineer knows everything is green. + +--- + +## 3. CLI / End-to-End Smoke +Run these in the repo root once the MCP tools succeed: + +```powershell +uv run code-index-mcp --project-path C:\Users\p10362321\project\code-index-mcp +uv run pytest +``` + +- Treat any warning or stderr output as a blocker. +- Capture timestamps + durations; attach to release prep if we are close to tagging. + +--- + +## 4. Communicate Status +When handing the session back to the team, summarize: + +- **SDK state**: Confirm we are still on MCP 1.21.0 (with context injection + capability helpers). +- **Tool cache**: Mention that clients should re-cache tool lists after restart (FastMCP now enforces metadata changes). +- **Known issues**: Note any skipped steps, flaky tests, or manual interventions. +- **Next action**: “Ready for release prep” or “Need follow-up on X” — whichever applies after the smoke tests. + +--- + +## 5. Troubleshooting Quick Reference +- **`set_project_path` fails** → Ensure the repo path is accessible (sandbox permissions) and no other agent locked `index.db`. Run `clear_settings()` then retry. +- **Search returns zero results** → Run `refresh_search_tools()`; if ripgrep missing, fall back to `basic` and flag the infra team. +- **Watcher inactive** → Call `configure_file_watcher(enabled=True)` and `refresh_index()`. Document if it remains inactive. +- **CLI smoke exits non-zero** → Capture full stdout/stderr, file an issue linked to `docs/mcp-upgrade-notes.md`, and pause release work. + +Keep this section updated with any new gotchas discovered during restarts. + +--- + +## 6. Hand-off Checklist +- [ ] Steps 1–4 executed and logged in the current session. +- [ ] Any deviations documented (include timestamps + command output). +- [ ] This playbook reviewed/updated if procedures changed. + +If all boxes are checked, the MCP server is considered healthy and ready for normal development or release activities. diff --git a/reference/code-index-mcp-master/docs/mcp-upgrade-notes.md b/reference/code-index-mcp-master/docs/mcp-upgrade-notes.md new file mode 100644 index 00000000..19dec3f5 --- /dev/null +++ b/reference/code-index-mcp-master/docs/mcp-upgrade-notes.md @@ -0,0 +1,28 @@ +# MCP Upgrade Notes (November 2025) + +## Why this upgrade matters +- `mcp` 1.21.0 was published to PyPI on 2025-11-06, so we are at least 17 point releases behind the current SDK and missing recent transport, auth, and client-surface fixes. +- The MCP governance group will cut the next specification release on 2025-11-25 (RC on 2025-11-11), so validating 1.21.0 now keeps us aligned ahead of another protocol bump. + +## Dependency & packaging considerations +1. Run `uv lock --upgrade mcp` (or equivalent) so `uv.lock` stops pinning 1.4.1 and picks up the 1.21.0 wheels plus their refreshed transitive set (Starlette 0.49.1, AnyIO/HTTPX upgrades, etc.). +2. Re-run `uv run pytest` and our smoke commands (`uv run code-index-mcp --project-path `) because AnyIO cancellation semantics and Starlette ASGI changes can surface subtle regressions in watcher services. +3. Publish the lockfile and version bumps together; our release checklist requires pyproject + package __init__ + uv.lock to stay in sync. + +## API & runtime changes to verify +- SEP-985 landed in 1.21.0, adding OAuth-protected resource metadata fallback: confirm our SettingsService handles `WWW-Authenticate` responses and that CLI flags surface any required bearer tokens. +- `ClientSession.get_server_capabilities()` is new; if clients or integration tests introspect capabilities manually, migrate to this helper. +- Starlette 0.49.1 ships tighter ASGI scope validation; double-check our SSE transport and progress notifications. + +## Recommended practices for 1.21.x +1. **Depend on Context injection, not globals.** Annotate `ctx: Context` parameters so FastMCP injects the request context automatically instead of calling `mcp.get_context()` directly; this keeps us compatible with async-only handlers and future dependency-injection changes. +2. **Cache expensive tool listings in clients.** Newer agents (OpenAI Agents SDK, Claude Desktop) call `list_tools()` on every run; set `cache_tools_list=True` only when our tool roster is static and call `invalidate_tools_cache()` after deployments. +3. **Respect capability negotiation each session.** Protocol version 2025-06-18 remains current, and version negotiation happens during `initialize`; ensure our server exposes accurate `capabilities` metadata and gracefully errors when clients offer only future versions. +4. **Stay ahead of November spec changes.** The upcoming 2025-11-25 spec focuses on additional security hardening. Schedule time to exercise the RC (available 2025-11-11) so we can absorb any required surface changes early. +5. **Document OAuth and transport choices.** With SEP-985 and other auth SEPs in flight, record which flows (`device`, `jwt-bearer`, etc.) each deployment expects, and prefer the Streamable HTTP transport when exposing remote servers to benefit from the latest security guidance. + +## Validation checklist before merging +- [ ] Lockfile regenerated (`uv lock --upgrade mcp`) and `uv run python -m code_index_mcp.server --help` still succeeds. +- [ ] `uv run code-index-mcp --project-path ` exercises `set_project_path`, `build_deep_index`, and `search_code_advanced` end-to-end. +- [ ] Smoke Claude Desktop / Codex CLI against the upgraded server; confirm resources + tools enumerate and that tool caching behaves as expected. +- [ ] Update release notes + AGENTS.md summary once 1.21.x is verified in staging. diff --git a/reference/code-index-mcp-master/fastmcp.json b/reference/code-index-mcp-master/fastmcp.json new file mode 100644 index 00000000..dd119a39 --- /dev/null +++ b/reference/code-index-mcp-master/fastmcp.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://fastmcp.wiki/en/schemas/fastmcp.json", + "name": "Code Index MCP", + "description": "Indexes a local repository and exposes search, indexing, and file utilities via the Model Context Protocol.", + "license": "MIT", + "keywords": [ + "mcp", + "code-index", + "search", + "fastmcp" + ], + "links": [ + { + "rel": "source", + "href": "https://github.com/johnhuang316/code-index-mcp" + }, + { + "rel": "documentation", + "href": "https://github.com/johnhuang316/code-index-mcp#readme" + } + ], + "source": { + "path": "src/code_index_mcp/server.py", + "entrypoint": "mcp" + }, + "environment": { + "python": ">=3.10", + "dependencies": [ + "mcp>=1.21.0,<2.0.0", + "watchdog>=3.0.0", + "tree-sitter>=0.20.0", + "tree-sitter-javascript>=0.20.0", + "tree-sitter-typescript>=0.20.0", + "tree-sitter-java>=0.20.0", + "tree-sitter-zig>=0.20.0", + "pathspec>=0.12.1", + "msgpack>=1.0.0" + ] + }, + "deployment": { + "transport": "stdio" + } +} diff --git a/reference/code-index-mcp-master/pyproject.toml b/reference/code-index-mcp-master/pyproject.toml new file mode 100644 index 00000000..84135c99 --- /dev/null +++ b/reference/code-index-mcp-master/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "code-index-mcp" +version = "2.9.4" +description = "Code indexing and analysis tools for LLMs using MCP" +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +authors = [ + {name = "johnhuang316"} +] +dependencies = [ + "mcp>=1.21.0,<2.0.0", + "watchdog>=3.0.0", + "tree-sitter>=0.20.0", + "tree-sitter-javascript>=0.20.0", + "tree-sitter-typescript>=0.20.0", + "tree-sitter-java>=0.20.0", + "tree-sitter-zig>=0.20.0", + "pathspec>=0.12.1", + "msgpack>=1.0.0", +] + +[project.urls] +Homepage = "https://github.com/johnhuang316/code-index-mcp" +"Bug Tracker" = "https://github.com/johnhuang316/code-index-mcp/issues" + +[project.scripts] +code-index-mcp = "code_index_mcp.server:main" + +[tool.setuptools] +package-dir = {"" = "src"} diff --git a/reference/code-index-mcp-master/requirements.txt b/reference/code-index-mcp-master/requirements.txt new file mode 100644 index 00000000..2298be7f --- /dev/null +++ b/reference/code-index-mcp-master/requirements.txt @@ -0,0 +1,10 @@ +mcp>=1.21.0,<2.0.0 +watchdog>=3.0.0 +protobuf>=4.21.0 +tree-sitter>=0.20.0 +tree-sitter-javascript>=0.20.0 +tree-sitter-typescript>=0.20.0 +tree-sitter-java>=0.20.0 +tree-sitter-zig>=0.20.0 +pathspec>=0.12.1 +libclang>=16.0.0 diff --git a/reference/code-index-mcp-master/run.py b/reference/code-index-mcp-master/run.py new file mode 100644 index 00000000..1303bfe6 --- /dev/null +++ b/reference/code-index-mcp-master/run.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +""" +Development convenience script to run the Code Index MCP server. +""" +import sys +import os + +# Add src directory to path +src_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src') +sys.path.insert(0, src_path) + +try: + from code_index_mcp.server import main + + if __name__ == "__main__": + main() +except Exception: + # Exit silently on failure without printing any messages + raise SystemExit(1) diff --git a/reference/code-index-mcp-master/src/code_index_mcp/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/__init__.py new file mode 100644 index 00000000..e55b5dd6 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/__init__.py @@ -0,0 +1,6 @@ +"""Code Index MCP package. + +A Model Context Protocol server for code indexing, searching, and analysis. +""" + +__version__ = "2.9.4" diff --git a/reference/code-index-mcp-master/src/code_index_mcp/__main__.py b/reference/code-index-mcp-master/src/code_index_mcp/__main__.py new file mode 100644 index 00000000..99b3b601 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/__main__.py @@ -0,0 +1,6 @@ +"""Main entry point for the code-index-mcp package.""" + +from code_index_mcp.server import main + +if __name__ == "__main__": + main() diff --git a/reference/code-index-mcp-master/src/code_index_mcp/constants.py b/reference/code-index-mcp-master/src/code_index_mcp/constants.py new file mode 100644 index 00000000..ddcdca59 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/constants.py @@ -0,0 +1,128 @@ +""" +Shared constants for the Code Index MCP server. +""" + +# Directory and file names +SETTINGS_DIR = "code_indexer" +CONFIG_FILE = "config.json" +INDEX_FILE = "index.json" # JSON index file (deep index) +INDEX_FILE_SHALLOW = "index.shallow.json" # Minimal shallow index (file list) +INDEX_FILE_DB = "index.db" # SQLite deep index file + +# Supported file extensions for code analysis +# This is the authoritative list used by both old and new indexing systems +SUPPORTED_EXTENSIONS = [ + # Core programming languages + '.py', '.pyw', # Python + '.js', '.jsx', '.ts', '.tsx', # JavaScript/TypeScript + '.mjs', '.cjs', # Modern JavaScript + '.java', # Java + '.c', '.cpp', '.h', '.hpp', # C/C++ + '.cxx', '.cc', '.hxx', '.hh', # C++ variants + '.cs', # C# + '.go', # Go + '.m', '.mm', # Objective-C + '.rb', # Ruby + '.php', # PHP + '.swift', # Swift + '.kt', '.kts', # Kotlin + '.rs', # Rust + '.scala', # Scala + '.sh', '.bash', '.zsh', # Shell scripts + '.ps1', # PowerShell + '.bat', '.cmd', # Windows batch + '.r', '.R', # R + '.pl', '.pm', # Perl + '.lua', # Lua + '.dart', # Dart + '.hs', # Haskell + '.ml', '.mli', # OCaml + '.fs', '.fsx', # F# + '.clj', '.cljs', # Clojure + '.vim', # Vim script + '.zig', '.zon', # Zig + + # Web and markup + '.html', '.htm', # HTML + '.css', '.scss', '.sass', # Stylesheets + '.less', '.stylus', '.styl', # Style languages + '.md', '.mdx', # Markdown + '.json', '.jsonc', # JSON + '.xml', # XML + '.yml', '.yaml', # YAML + + # Frontend frameworks + '.vue', # Vue.js + '.svelte', # Svelte + '.astro', # Astro + + # Java web & build artifacts + '.jsp', '.jspx', '.jspf', # JSP pages + '.tag', '.tagx', # JSP tag files + '.gsp', # Grails templates + '.properties', # Java .properties configs + '.gradle', '.groovy', # Gradle/Groovy build scripts + '.proto', # Protocol Buffers + + # Template engines + '.hbs', '.handlebars', # Handlebars + '.ejs', # EJS + '.pug', # Pug + '.ftl', # FreeMarker + '.mustache', '.liquid', '.erb', # Additional template engines + + # Database and SQL + '.sql', '.ddl', '.dml', # SQL + '.mysql', '.postgresql', '.psql', # Database-specific SQL + '.sqlite', '.mssql', '.oracle', # More databases + '.ora', '.db2', # Oracle and DB2 + '.proc', '.procedure', # Stored procedures + '.func', '.function', # Functions + '.view', '.trigger', '.index', # Database objects + '.migration', '.seed', '.fixture', # Migration files + '.schema', # Schema files + '.cql', '.cypher', '.sparql', # NoSQL query languages + '.gql', # GraphQL + '.liquibase', '.flyway', # Migration tools +] + +# Centralized filtering configuration +FILTER_CONFIG = { + "exclude_directories": { + # Version control + '.git', '.svn', '.hg', '.bzr', + + # Package managers & dependencies + 'node_modules', '__pycache__', '.venv', 'venv', + 'vendor', 'bower_components', + + # Build outputs + 'dist', 'build', 'target', 'out', 'bin', 'obj', + + # IDE & editors + '.idea', '.vscode', '.vs', '.sublime-workspace', + + # Testing & coverage + '.pytest_cache', '.coverage', '.tox', '.nyc_output', + 'coverage', 'htmlcov', + + # OS artifacts + '.DS_Store', 'Thumbs.db', 'desktop.ini' + }, + + "exclude_files": { + # Temporary files + '*.tmp', '*.temp', '*.swp', '*.swo', + + # Backup files + '*.bak', '*~', '*.orig', + + # Log files + '*.log', + + # Lock files + 'package-lock.json', 'yarn.lock', 'Pipfile.lock' + }, + + "supported_extensions": SUPPORTED_EXTENSIONS +} diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/__init__.py new file mode 100644 index 00000000..8201506d --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/__init__.py @@ -0,0 +1,36 @@ +""" +Code indexing utilities for the MCP server. + +Deep indexing now relies exclusively on the SQLite backend. +""" + +from .qualified_names import generate_qualified_name, normalize_file_path +from .json_index_builder import JSONIndexBuilder, IndexMetadata +from .sqlite_index_builder import SQLiteIndexBuilder +from .sqlite_index_manager import SQLiteIndexManager +from .shallow_index_manager import ShallowIndexManager, get_shallow_index_manager +from .deep_index_manager import DeepIndexManager +from .models import SymbolInfo, FileInfo + +_sqlite_index_manager = SQLiteIndexManager() + + +def get_index_manager() -> SQLiteIndexManager: + """Return the singleton SQLite index manager.""" + return _sqlite_index_manager + + +__all__ = [ + "generate_qualified_name", + "normalize_file_path", + "JSONIndexBuilder", + "IndexMetadata", + "SQLiteIndexBuilder", + "SQLiteIndexManager", + "get_index_manager", + "ShallowIndexManager", + "get_shallow_index_manager", + "DeepIndexManager", + "SymbolInfo", + "FileInfo", +] diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/deep_index_manager.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/deep_index_manager.py new file mode 100644 index 00000000..2ee7d7f0 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/deep_index_manager.py @@ -0,0 +1,44 @@ +""" +Deep Index Manager - Wrapper around the SQLite index manager. + +This class provides a clear semantic separation from the shallow manager while +delegating operations to the SQLite-backed implementation. +""" + +from __future__ import annotations + +from typing import Optional, Dict, Any, List + +from .sqlite_index_manager import SQLiteIndexManager + + +class DeepIndexManager: + """Thin wrapper over SQLiteIndexManager to expose deep-index API.""" + + def __init__(self) -> None: + self._mgr = SQLiteIndexManager() + + # Expose a subset of API to keep callers simple + def set_project_path(self, project_path: str) -> bool: + return self._mgr.set_project_path(project_path) + + def build_index(self, force_rebuild: bool = False) -> bool: + return self._mgr.build_index(force_rebuild=force_rebuild) + + def load_index(self) -> bool: + return self._mgr.load_index() + + def refresh_index(self) -> bool: + return self._mgr.refresh_index() + + def find_files(self, pattern: str = "*") -> List[str]: + return self._mgr.find_files(pattern) + + def get_file_summary(self, file_path: str) -> Optional[Dict[str, Any]]: + return self._mgr.get_file_summary(file_path) + + def get_index_stats(self) -> Dict[str, Any]: + return self._mgr.get_index_stats() + + def cleanup(self) -> None: + self._mgr.cleanup() diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/index_provider.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/index_provider.py new file mode 100644 index 00000000..660bb8d4 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/index_provider.py @@ -0,0 +1,125 @@ +""" +Index provider interface definitions. + +Defines standard interfaces for all index access, ensuring consistency across different implementations. +""" + +from typing import List, Optional, Dict, Any, Protocol +from dataclasses import dataclass + +from .models import SymbolInfo, FileInfo + + +@dataclass +class IndexMetadata: + """Standard index metadata structure.""" + version: str + format_type: str + created_at: float + last_updated: float + file_count: int + project_root: str + tool_version: str + + +class IIndexProvider(Protocol): + """ + Standard index provider interface. + + All index implementations must follow this interface to ensure consistent access patterns. + """ + + def get_file_list(self) -> List[FileInfo]: + """ + Get list of all indexed files. + + Returns: + List of file information objects + """ + ... + + def get_file_info(self, file_path: str) -> Optional[FileInfo]: + """ + Get information for a specific file. + + Args: + file_path: Relative file path + + Returns: + File information, or None if file is not in index + """ + ... + + def query_symbols(self, file_path: str) -> List[SymbolInfo]: + """ + Query symbol information in a file. + + Args: + file_path: Relative file path + + Returns: + List of symbol information objects + """ + ... + + def search_files(self, pattern: str) -> List[str]: + """ + Search files by pattern. + + Args: + pattern: Glob pattern or regular expression + + Returns: + List of matching file paths + """ + ... + + def get_metadata(self) -> IndexMetadata: + """ + Get index metadata. + + Returns: + Index metadata information + """ + ... + + def is_available(self) -> bool: + """ + Check if index is available. + + Returns: + True if index is available and functional + """ + ... + + +class IIndexManager(Protocol): + """ + Index manager interface. + + Defines standard interface for index lifecycle management. + """ + + def initialize(self) -> bool: + """Initialize the index manager.""" + ... + + def get_provider(self) -> Optional[IIndexProvider]: + """Get the current active index provider.""" + ... + + def refresh_index(self, force: bool = False) -> bool: + """Refresh the index.""" + ... + + def save_index(self) -> bool: + """Save index state.""" + ... + + def clear_index(self) -> None: + """Clear index state.""" + ... + + def get_index_status(self) -> Dict[str, Any]: + """Get index status information.""" + ... diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/json_index_builder.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/json_index_builder.py new file mode 100644 index 00000000..493fb1d2 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/json_index_builder.py @@ -0,0 +1,472 @@ +""" +JSON Index Builder - Clean implementation using Strategy pattern. + +This replaces the monolithic parser implementation with a clean, +maintainable Strategy pattern architecture. +""" + +import logging +import os +import time +from collections import defaultdict +from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed +from dataclasses import dataclass, asdict +from pathlib import Path +from typing import Dict, List, Optional, Any, Tuple + +from .strategies import StrategyFactory +from .models import SymbolInfo, FileInfo + +logger = logging.getLogger(__name__) + + +@dataclass +class IndexMetadata: + """Metadata for the JSON index.""" + project_path: str + indexed_files: int + index_version: str + timestamp: str + languages: List[str] + total_symbols: int = 0 + specialized_parsers: int = 0 + fallback_files: int = 0 + + +class JSONIndexBuilder: + """ + Main index builder using Strategy pattern for language parsing. + + This class orchestrates the index building process by: + 1. Discovering files in the project + 2. Using StrategyFactory to get appropriate parsers + 3. Extracting symbols and metadata + 4. Assembling the final JSON index + """ + + def __init__(self, project_path: str, additional_excludes: Optional[List[str]] = None): + from ..utils import FileFilter + + # Input validation + if not isinstance(project_path, str): + raise ValueError(f"Project path must be a string, got {type(project_path)}") + + project_path = project_path.strip() + if not project_path: + raise ValueError("Project path cannot be empty") + + if not os.path.isdir(project_path): + raise ValueError(f"Project path does not exist: {project_path}") + + self.project_path = project_path + self.in_memory_index: Optional[Dict[str, Any]] = None + self.strategy_factory = StrategyFactory() + self.file_filter = FileFilter(additional_excludes) + + logger.info(f"Initialized JSON index builder for {project_path}") + strategy_info = self.strategy_factory.get_strategy_info() + logger.info(f"Available parsing strategies: {len(strategy_info)} types") + + # Log specialized vs fallback coverage + specialized = len(self.strategy_factory.get_specialized_extensions()) + fallback = len(self.strategy_factory.get_fallback_extensions()) + logger.info(f"Specialized parsers: {specialized} extensions, Fallback coverage: {fallback} extensions") + + def _process_file(self, file_path: str, specialized_extensions: set) -> Optional[Tuple[Dict, Dict, str, bool]]: + """ + Process a single file - designed for parallel execution. + + Args: + file_path: Path to the file to process + specialized_extensions: Set of extensions with specialized parsers + + Returns: + Tuple of (symbols, file_info, language, is_specialized) or None on error + """ + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + ext = Path(file_path).suffix.lower() + rel_path = os.path.relpath(file_path, self.project_path).replace('\\', '/') + + # Get appropriate strategy + strategy = self.strategy_factory.get_strategy(ext) + + # Track strategy usage + is_specialized = ext in specialized_extensions + + # Parse file using strategy + symbols, file_info = strategy.parse_file(rel_path, content) + + logger.debug(f"Parsed {rel_path}: {len(symbols)} symbols ({file_info.language})") + + return (symbols, {rel_path: file_info}, file_info.language, is_specialized) + + except Exception as e: + logger.warning(f"Error processing {file_path}: {e}") + return None + + def build_index(self, parallel: bool = True, max_workers: Optional[int] = None) -> Dict[str, Any]: + """ + Build the complete index using Strategy pattern with parallel processing. + + Args: + parallel: Whether to use parallel processing (default: True) + max_workers: Maximum number of worker processes/threads (default: CPU count) + + Returns: + Complete JSON index with metadata, symbols, and file information + """ + logger.info(f"Building JSON index using Strategy pattern (parallel={parallel})...") + start_time = time.time() + + all_symbols = {} + all_files = {} + languages = set() + specialized_count = 0 + fallback_count = 0 + pending_calls: List[Tuple[str, str]] = [] + + # Get specialized extensions for tracking + specialized_extensions = set(self.strategy_factory.get_specialized_extensions()) + + # Get list of files to process + files_to_process = self._get_supported_files() + total_files = len(files_to_process) + + if total_files == 0: + logger.warning("No files to process") + return self._create_empty_index() + + logger.info(f"Processing {total_files} files...") + + def process_result(result): + nonlocal specialized_count, fallback_count + if not result: + return + symbols, file_info_dict, language, is_specialized = result + for symbol_id, symbol_info in symbols.items(): + all_symbols[symbol_id] = symbol_info + for rel_path, file_info in file_info_dict.items(): + all_files[rel_path] = file_info + file_pending = getattr(file_info, "pending_calls", []) + if file_pending: + pending_calls.extend(file_pending) + languages.add(language) + if is_specialized: + specialized_count += 1 + else: + fallback_count += 1 + + if parallel and total_files > 1: + # Use ThreadPoolExecutor for I/O-bound file reading + # ProcessPoolExecutor has issues with strategy sharing + if max_workers is None: + max_workers = min(os.cpu_count() or 4, total_files) + + logger.info(f"Using parallel processing with {max_workers} workers") + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all tasks + future_to_file = { + executor.submit(self._process_file, file_path, specialized_extensions): file_path + for file_path in files_to_process + } + + # Process completed tasks + processed = 0 + for future in as_completed(future_to_file): + file_path = future_to_file[future] + result = future.result() + + process_result(result) + + processed += 1 + if processed % 100 == 0: + logger.debug(f"Processed {processed}/{total_files} files") + else: + # Sequential processing + logger.info("Using sequential processing") + for file_path in files_to_process: + result = self._process_file(file_path, specialized_extensions) + process_result(result) + + self._resolve_pending_calls(all_symbols, pending_calls) + + # Build index metadata + metadata = IndexMetadata( + project_path=self.project_path, + indexed_files=len(all_files), + index_version="2.0.0-strategy", + timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + languages=sorted(list(languages)), + total_symbols=len(all_symbols), + specialized_parsers=specialized_count, + fallback_files=fallback_count + ) + + # Assemble final index + index = { + "metadata": asdict(metadata), + "symbols": {k: asdict(v) for k, v in all_symbols.items()}, + "files": {k: asdict(v) for k, v in all_files.items()} + } + + # Cache in memory + self.in_memory_index = index + + elapsed = time.time() - start_time + logger.info(f"Built index with {len(all_symbols)} symbols from {len(all_files)} files in {elapsed:.2f}s") + logger.info(f"Languages detected: {sorted(languages)}") + logger.info(f"Strategy usage: {specialized_count} specialized, {fallback_count} fallback") + + return index + + def _resolve_pending_calls( + self, + all_symbols: Dict[str, SymbolInfo], + pending_calls: List[Tuple[str, str]] + ) -> None: + """Resolve cross-file call relationships using global symbol index.""" + if not pending_calls: + return + + short_index: Dict[str, List[str]] = defaultdict(list) + for symbol_id in all_symbols: + short_name = symbol_id.split("::")[-1] + short_index[short_name].append(symbol_id) + + for caller, called in pending_calls: + target_ids: List[str] = [] + if called in all_symbols: + target_ids = [called] + else: + if called in short_index: + target_ids = short_index[called] + if not target_ids and "." in called: + target_ids = short_index.get(called, []) + if not target_ids: + matches: List[str] = [] + suffix = f".{called}" + for short_name, ids in short_index.items(): + if short_name.endswith(suffix): + matches.extend(ids) + target_ids = matches + + if len(target_ids) != 1: + continue + + symbol_info = all_symbols[target_ids[0]] + if caller not in symbol_info.called_by: + symbol_info.called_by.append(caller) + + def _create_empty_index(self) -> Dict[str, Any]: + """Create an empty index structure.""" + metadata = IndexMetadata( + project_path=self.project_path, + indexed_files=0, + index_version="2.0.0-strategy", + timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + languages=[], + total_symbols=0, + specialized_parsers=0, + fallback_files=0 + ) + + return { + "metadata": asdict(metadata), + "symbols": {}, + "files": {} + } + + def get_index(self) -> Optional[Dict[str, Any]]: + """Get the current in-memory index.""" + return self.in_memory_index + + def clear_index(self): + """Clear the in-memory index.""" + self.in_memory_index = None + logger.debug("Cleared in-memory index") + + def _get_supported_files(self) -> List[str]: + """ + Get all supported files in the project using centralized filtering. + + Returns: + List of file paths that can be parsed + """ + supported_files = [] + base_path = Path(self.project_path) + + try: + for root, dirs, files in os.walk(self.project_path): + # Filter directories in-place using centralized logic + dirs[:] = [d for d in dirs if not self.file_filter.should_exclude_directory(d)] + + # Filter files using centralized logic + for file in files: + file_path = Path(root) / file + if self.file_filter.should_process_path(file_path, base_path): + supported_files.append(str(file_path)) + + except Exception as e: + logger.error(f"Error scanning directory {self.project_path}: {e}") + + logger.debug(f"Found {len(supported_files)} supported files") + return supported_files + + def build_shallow_file_list(self) -> List[str]: + """ + Build a minimal shallow index consisting of relative file paths only. + + This method does not read file contents. It enumerates supported files + using centralized filtering and returns normalized relative paths with + forward slashes for cross-platform consistency. + + Returns: + List of relative file paths (using '/'). + """ + try: + absolute_files = self._get_supported_files() + result: List[str] = [] + for abs_path in absolute_files: + rel_path = os.path.relpath(abs_path, self.project_path).replace('\\', '/') + # Normalize leading './' + if rel_path.startswith('./'): + rel_path = rel_path[2:] + result.append(rel_path) + return result + except Exception as e: + logger.error(f"Failed to build shallow file list: {e}") + return [] + + def save_index(self, index: Dict[str, Any], index_path: str) -> bool: + """ + Save index to disk. + + Args: + index: Index data to save + index_path: Path where to save the index + + Returns: + True if successful, False otherwise + """ + try: + import json + with open(index_path, 'w', encoding='utf-8') as f: + json.dump(index, f, indent=2, ensure_ascii=False) + logger.info(f"Saved index to {index_path}") + return True + except Exception as e: + logger.error(f"Failed to save index to {index_path}: {e}") + return False + + def load_index(self, index_path: str) -> Optional[Dict[str, Any]]: + """ + Load index from disk. + + Args: + index_path: Path to the index file + + Returns: + Index data if successful, None otherwise + """ + try: + if not os.path.exists(index_path): + logger.debug(f"Index file not found: {index_path}") + return None + + import json + with open(index_path, 'r', encoding='utf-8') as f: + index = json.load(f) + + # Cache in memory + self.in_memory_index = index + logger.info(f"Loaded index from {index_path}") + return index + + except Exception as e: + logger.error(f"Failed to load index from {index_path}: {e}") + return None + + def get_parsing_statistics(self) -> Dict[str, Any]: + """ + Get detailed statistics about parsing capabilities. + + Returns: + Dictionary with parsing statistics and strategy information + """ + strategy_info = self.strategy_factory.get_strategy_info() + + return { + "total_strategies": len(strategy_info), + "specialized_languages": [lang for lang in strategy_info.keys() if not lang.startswith('fallback_')], + "fallback_languages": [lang.replace('fallback_', '') for lang in strategy_info.keys() if lang.startswith('fallback_')], + "total_extensions": len(self.strategy_factory.get_all_supported_extensions()), + "specialized_extensions": len(self.strategy_factory.get_specialized_extensions()), + "fallback_extensions": len(self.strategy_factory.get_fallback_extensions()), + "strategy_details": strategy_info + } + + def get_file_symbols(self, file_path: str) -> List[Dict[str, Any]]: + """ + Get symbols for a specific file. + + Args: + file_path: Relative path to the file + + Returns: + List of symbols in the file + """ + if not self.in_memory_index: + logger.warning("Index not loaded") + return [] + + try: + # Normalize file path + file_path = file_path.replace('\\', '/') + if file_path.startswith('./'): + file_path = file_path[2:] + + # Get file info + file_info = self.in_memory_index["files"].get(file_path) + if not file_info: + logger.warning(f"File not found in index: {file_path}") + return [] + + # Work directly with global symbols for this file + global_symbols = self.in_memory_index.get("symbols", {}) + result = [] + + # Find all symbols for this file directly from global symbols + for symbol_id, symbol_data in global_symbols.items(): + symbol_file = symbol_data.get("file", "").replace("\\", "/") + + # Check if this symbol belongs to our file + if symbol_file == file_path: + symbol_type = symbol_data.get("type", "unknown") + symbol_name = symbol_id.split("::")[-1] # Extract symbol name from ID + + # Create symbol info + symbol_info = { + "name": symbol_name, + "called_by": symbol_data.get("called_by", []), + "line": symbol_data.get("line"), + "signature": symbol_data.get("signature") + } + + # Categorize by type + if symbol_type in ["function", "method"]: + result.append(symbol_info) + elif symbol_type == "class": + result.append(symbol_info) + + # Sort by line number for consistent ordering + result.sort(key=lambda x: x.get("line", 0)) + + return result + + except Exception as e: + logger.error(f"Error getting file symbols for {file_path}: {e}") + return [] diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/__init__.py new file mode 100644 index 00000000..b120a345 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/__init__.py @@ -0,0 +1,8 @@ +""" +Model classes for the indexing system. +""" + +from .symbol_info import SymbolInfo +from .file_info import FileInfo + +__all__ = ['SymbolInfo', 'FileInfo'] \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/file_info.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/file_info.py new file mode 100644 index 00000000..06787743 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/file_info.py @@ -0,0 +1,24 @@ +""" +FileInfo model for representing file metadata. +""" + +from dataclasses import dataclass +from typing import Dict, List, Optional, Any + + +@dataclass +class FileInfo: + """Information about a source code file.""" + + language: str # programming language + line_count: int # total lines in file + symbols: Dict[str, List[str]] # symbol categories (functions, classes, etc.) + imports: List[str] # imported modules/packages + exports: Optional[List[str]] = None # exported symbols (for JS/TS modules) + package: Optional[str] = None # package name (for Java, Go, etc.) + docstring: Optional[str] = None # file-level documentation + + def __post_init__(self): + """Initialize mutable defaults.""" + if self.exports is None: + self.exports = [] \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/symbol_info.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/symbol_info.py new file mode 100644 index 00000000..16593308 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/models/symbol_info.py @@ -0,0 +1,23 @@ +""" +SymbolInfo model for representing code symbols. +""" + +from dataclasses import dataclass +from typing import Optional, List + + +@dataclass +class SymbolInfo: + """Information about a code symbol (function, class, method, etc.).""" + + type: str # function, class, method, interface, etc. + file: str # file path where symbol is defined + line: int # line number where symbol starts + signature: Optional[str] = None # function/method signature + docstring: Optional[str] = None # documentation string + called_by: Optional[List[str]] = None # list of symbols that call this symbol + + def __post_init__(self): + """Initialize mutable defaults.""" + if self.called_by is None: + self.called_by = [] \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/qualified_names.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/qualified_names.py new file mode 100644 index 00000000..18e108ce --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/qualified_names.py @@ -0,0 +1,49 @@ +""" +Qualified name generation utilities. +""" +import os +from typing import Optional + + +def normalize_file_path(file_path: str) -> str: + """ + Normalize a file path to use forward slashes and relative paths. + + Args: + file_path: The file path to normalize + + Returns: + Normalized file path + """ + # Convert to forward slashes and make relative + normalized = file_path.replace('\\', '/') + + # Remove leading slash if present + if normalized.startswith('/'): + normalized = normalized[1:] + + return normalized + + +def generate_qualified_name(file_path: str, symbol_name: str, namespace: Optional[str] = None) -> str: + """ + Generate a qualified name for a symbol. + + Args: + file_path: Path to the file containing the symbol + symbol_name: Name of the symbol + namespace: Optional namespace/module context + + Returns: + Qualified name for the symbol + """ + normalized_path = normalize_file_path(file_path) + + # Remove file extension for module-like name + base_name = os.path.splitext(normalized_path)[0] + module_path = base_name.replace('/', '.') + + if namespace: + return f"{module_path}.{namespace}.{symbol_name}" + else: + return f"{module_path}.{symbol_name}" \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/shallow_index_manager.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/shallow_index_manager.py new file mode 100644 index 00000000..48ad4b48 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/shallow_index_manager.py @@ -0,0 +1,194 @@ +""" +Shallow Index Manager - Manages a minimal file-list-only index. + +This manager builds and loads a shallow index consisting of relative file +paths only. It is optimized for fast initialization and filename-based +search/browsing. Content parsing and symbol extraction are not performed. +""" + +from __future__ import annotations + +import hashlib +import json +import logging +import os +import tempfile +import threading +from typing import List, Optional +import re + +from .json_index_builder import JSONIndexBuilder +from ..constants import SETTINGS_DIR, INDEX_FILE_SHALLOW + +logger = logging.getLogger(__name__) + + +class ShallowIndexManager: + """Manage shallow (file-list) index lifecycle and storage.""" + + def __init__(self) -> None: + self.project_path: Optional[str] = None + self.index_builder: Optional[JSONIndexBuilder] = None + self.temp_dir: Optional[str] = None + self.index_path: Optional[str] = None + self._file_list: Optional[List[str]] = None + self._lock = threading.RLock() + + def set_project_path(self, project_path: str) -> bool: + with self._lock: + try: + if not isinstance(project_path, str) or not project_path.strip(): + logger.error("Invalid project path for shallow index") + return False + project_path = project_path.strip() + if not os.path.isdir(project_path): + logger.error(f"Project path does not exist: {project_path}") + return False + + self.project_path = project_path + self.index_builder = JSONIndexBuilder(project_path) + + project_hash = hashlib.md5(project_path.encode()).hexdigest()[:12] + self.temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR, project_hash) + os.makedirs(self.temp_dir, exist_ok=True) + self.index_path = os.path.join(self.temp_dir, INDEX_FILE_SHALLOW) + return True + except Exception as e: # noqa: BLE001 - centralized logging + logger.error(f"Failed to set project path (shallow): {e}") + return False + + def build_index(self) -> bool: + """Build and persist the shallow file list index.""" + with self._lock: + if not self.index_builder or not self.index_path: + logger.error("ShallowIndexManager not initialized") + return False + try: + file_list = self.index_builder.build_shallow_file_list() + with open(self.index_path, 'w', encoding='utf-8') as f: + json.dump(file_list, f, ensure_ascii=False) + self._file_list = file_list + logger.info(f"Built shallow index with {len(file_list)} files") + return True + except Exception as e: # noqa: BLE001 + logger.error(f"Failed to build shallow index: {e}") + return False + + def load_index(self) -> bool: + """Load shallow index from disk to memory.""" + with self._lock: + try: + if not self.index_path or not os.path.exists(self.index_path): + return False + with open(self.index_path, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, list): + # Normalize slashes/prefix + normalized: List[str] = [] + for p in data: + if isinstance(p, str): + q = p.replace('\\\\', '/').replace('\\', '/') + if q.startswith('./'): + q = q[2:] + normalized.append(q) + self._file_list = normalized + return True + return False + except Exception as e: # noqa: BLE001 + logger.error(f"Failed to load shallow index: {e}") + return False + + def get_file_list(self) -> List[str]: + with self._lock: + return list(self._file_list or []) + + def find_files(self, pattern: str = "*") -> List[str]: + with self._lock: + if not isinstance(pattern, str): + return [] + norm = (pattern.strip() or "*").replace('\\\\','/').replace('\\','/') + files = self._file_list or [] + + # Fast path: wildcard all + if norm == "*": + return list(files) + + # 1) Exact, case-sensitive + exact_regex = self._compile_glob_regex(norm) + exact_hits = [f for f in files if exact_regex.match(f) is not None] + if exact_hits or '/' in norm: + return exact_hits + + # 2) Recursive **/ fallback (case-sensitive) + recursive_pattern = f"**/{norm}" + rec_regex = self._compile_glob_regex(recursive_pattern) + rec_hits = [f for f in files if rec_regex.match(f) is not None] + if rec_hits: + return self._dedupe_preserve_order(exact_hits + rec_hits) + + # 3) Case-insensitive (root only) + ci_regex = self._compile_glob_regex(norm, ignore_case=True) + ci_hits = [f for f in files if ci_regex.match(f) is not None] + if ci_hits: + return self._dedupe_preserve_order(exact_hits + rec_hits + ci_hits) + + # 4) Case-insensitive recursive + rec_ci_regex = self._compile_glob_regex(recursive_pattern, ignore_case=True) + rec_ci_hits = [f for f in files if rec_ci_regex.match(f) is not None] + if rec_ci_hits: + return self._dedupe_preserve_order( + exact_hits + rec_hits + ci_hits + rec_ci_hits + ) + + return [] + + @staticmethod + def _compile_glob_regex(pattern: str, ignore_case: bool = False) -> re.Pattern: + i = 0 + out = [] + special = ".^$+{}[]|()" + while i < len(pattern): + c = pattern[i] + if c == '*': + if i + 1 < len(pattern) and pattern[i + 1] == '*': + out.append('.*') + i += 2 + continue + else: + out.append('[^/]*') + elif c == '?': + out.append('[^/]') + elif c in special: + out.append('\\' + c) + else: + out.append(c) + i += 1 + flags = re.IGNORECASE if ignore_case else 0 + return re.compile('^' + ''.join(out) + '$', flags=flags) + + @staticmethod + def _dedupe_preserve_order(items: List[str]) -> List[str]: + seen = set() + result = [] + for item in items: + if item not in seen: + seen.add(item) + result.append(item) + return result + + def cleanup(self) -> None: + with self._lock: + self.project_path = None + self.index_builder = None + self.temp_dir = None + self.index_path = None + self._file_list = None + + +# Global singleton +_shallow_manager = ShallowIndexManager() + + +def get_shallow_index_manager() -> ShallowIndexManager: + return _shallow_manager + diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_index_builder.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_index_builder.py new file mode 100644 index 00000000..1e6b16a6 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_index_builder.py @@ -0,0 +1,327 @@ +""" +SQLite-backed index builder leveraging existing strategy pipeline. +""" + +from __future__ import annotations + +import json +import logging +import os +import time +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import Dict, Iterable, List, Optional, Tuple + +from .json_index_builder import JSONIndexBuilder +from .sqlite_store import SQLiteIndexStore +from .models import FileInfo, SymbolInfo + +logger = logging.getLogger(__name__) + + +class SQLiteIndexBuilder(JSONIndexBuilder): + """ + Build the deep index directly into SQLite storage. + + Inherits scanning/strategy utilities from JSONIndexBuilder but writes rows + to the provided SQLiteIndexStore instead of assembling large dictionaries. + """ + + def __init__( + self, + project_path: str, + store: SQLiteIndexStore, + additional_excludes: Optional[List[str]] = None, + ): + super().__init__(project_path, additional_excludes) + self.store = store + + def build_index( + self, + parallel: bool = True, + max_workers: Optional[int] = None, + ) -> Dict[str, int]: + """ + Build the SQLite index and return lightweight statistics. + + Args: + parallel: Whether to parse files in parallel. + max_workers: Optional override for worker count. + + Returns: + Dictionary with totals for files, symbols, and languages. + """ + logger.info("Building SQLite index (parallel=%s)...", parallel) + start_time = time.time() + + files_to_process = self._get_supported_files() + total_files = len(files_to_process) + if total_files == 0: + logger.warning("No files to process") + with self.store.connect(for_build=True) as conn: + self._reset_database(conn) + self._persist_metadata(conn, 0, 0, [], 0, 0, {}) + return { + "files": 0, + "symbols": 0, + "languages": 0, + } + + specialized_extensions = set(self.strategy_factory.get_specialized_extensions()) + + results_iter: Iterable[Tuple[Dict[str, SymbolInfo], Dict[str, FileInfo], str, bool]] + + executor = None + + if parallel and total_files > 1: + if max_workers is None: + max_workers = min(os.cpu_count() or 4, total_files) + logger.info("Using ThreadPoolExecutor with %s workers", max_workers) + executor = ThreadPoolExecutor(max_workers=max_workers) + future_to_file = { + executor.submit(self._process_file, file_path, specialized_extensions): file_path + for file_path in files_to_process + } + + def _iter_results(): + for future in as_completed(future_to_file): + result = future.result() + if result: + yield result + + results_iter = _iter_results() + else: + logger.info("Using sequential processing") + + def _iter_results_sequential(): + for file_path in files_to_process: + result = self._process_file(file_path, specialized_extensions) + if result: + yield result + + results_iter = _iter_results_sequential() + + languages = set() + specialized_count = 0 + fallback_count = 0 + pending_calls: List[Tuple[str, str]] = [] + total_symbols = 0 + symbol_types: Dict[str, int] = {} + processed_files = 0 + + self.store.initialize_schema() + with self.store.connect(for_build=True) as conn: + conn.execute("PRAGMA foreign_keys=ON") + self._reset_database(conn) + + for symbols, file_info_dict, language, is_specialized in results_iter: + file_path, file_info = next(iter(file_info_dict.items())) + file_id = self._insert_file(conn, file_path, file_info) + file_pending = getattr(file_info, "pending_calls", []) + if file_pending: + pending_calls.extend(file_pending) + symbol_rows = self._prepare_symbol_rows(symbols, file_id) + + if symbol_rows: + conn.executemany( + """ + INSERT INTO symbols( + symbol_id, + file_id, + type, + line, + signature, + docstring, + called_by, + short_name + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + symbol_rows, + ) + + languages.add(language) + processed_files += 1 + total_symbols += len(symbol_rows) + + if is_specialized: + specialized_count += 1 + else: + fallback_count += 1 + + for _, _, symbol_type, _, _, _, _, _ in symbol_rows: + key = symbol_type or "unknown" + symbol_types[key] = symbol_types.get(key, 0) + 1 + + self._persist_metadata( + conn, + processed_files, + total_symbols, + sorted(languages), + specialized_count, + fallback_count, + symbol_types, + ) + self._resolve_pending_calls_sqlite(conn, pending_calls) + try: + conn.execute("PRAGMA optimize") + except Exception: # pragma: no cover - best effort + pass + + if executor: + executor.shutdown(wait=True) + + elapsed = time.time() - start_time + logger.info( + "SQLite index built: files=%s symbols=%s languages=%s elapsed=%.2fs", + processed_files, + total_symbols, + len(languages), + elapsed, + ) + + return { + "files": processed_files, + "symbols": total_symbols, + "languages": len(languages), + } + + # Internal helpers ------------------------------------------------- + + def _reset_database(self, conn): + conn.execute("DELETE FROM symbols") + conn.execute("DELETE FROM files") + conn.execute( + "DELETE FROM metadata WHERE key NOT IN ('schema_version')" + ) + + def _insert_file(self, conn, path: str, file_info: FileInfo) -> int: + params = ( + path, + file_info.language, + file_info.line_count, + json.dumps(file_info.imports or []), + json.dumps(file_info.exports or []), + file_info.package, + file_info.docstring, + ) + cur = conn.execute( + """ + INSERT INTO files( + path, + language, + line_count, + imports, + exports, + package, + docstring + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """, + params, + ) + return cur.lastrowid + + def _prepare_symbol_rows( + self, + symbols: Dict[str, SymbolInfo], + file_id: int, + ) -> List[Tuple[str, int, Optional[str], Optional[int], Optional[str], Optional[str], str, str]]: + rows: List[Tuple[str, int, Optional[str], Optional[int], Optional[str], Optional[str], str, str]] = [] + for symbol_id, symbol_info in symbols.items(): + called_by = json.dumps(symbol_info.called_by or []) + short_name = symbol_id.split("::")[-1] + rows.append( + ( + symbol_id, + file_id, + symbol_info.type, + symbol_info.line, + symbol_info.signature, + symbol_info.docstring, + called_by, + short_name, + ) + ) + return rows + + def _persist_metadata( + self, + conn, + file_count: int, + symbol_count: int, + languages: List[str], + specialized_count: int, + fallback_count: int, + symbol_types: Dict[str, int], + ) -> None: + metadata = { + "project_path": self.project_path, + "indexed_files": file_count, + "index_version": "3.0.0-sqlite", + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "languages": languages, + "total_symbols": symbol_count, + "specialized_parsers": specialized_count, + "fallback_files": fallback_count, + "symbol_types": symbol_types, + } + self.store.set_metadata(conn, "project_path", self.project_path) + self.store.set_metadata(conn, "index_metadata", metadata) + + def _resolve_pending_calls_sqlite( + self, + conn, + pending_calls: List[Tuple[str, str]] + ) -> None: + """Resolve cross-file call relationships directly in SQLite storage.""" + if not pending_calls: + return + + rows = list( + conn.execute( + "SELECT symbol_id, short_name, called_by FROM symbols" + ) + ) + symbol_map = {row["symbol_id"]: row for row in rows} + short_index: Dict[str, List[str]] = defaultdict(list) + for row in rows: + short_name = row["short_name"] + if short_name: + short_index[short_name].append(row["symbol_id"]) + + updates: Dict[str, set] = defaultdict(set) + + for caller, called in pending_calls: + target_ids: List[str] = [] + if called in symbol_map: + target_ids = [called] + else: + if called in short_index: + target_ids = short_index[called] + if not target_ids: + suffix = f".{called}" + matches: List[str] = [] + for short_name, ids in short_index.items(): + if short_name and short_name.endswith(suffix): + matches.extend(ids) + target_ids = matches + + if len(target_ids) != 1: + continue + + updates[target_ids[0]].add(caller) + + for symbol_id, callers in updates.items(): + row = symbol_map.get(symbol_id) + if not row: + continue + existing = [] + if row["called_by"]: + try: + existing = json.loads(row["called_by"]) + except json.JSONDecodeError: + existing = [] + merged = list(dict.fromkeys(existing + list(callers))) + conn.execute( + "UPDATE symbols SET called_by=? WHERE symbol_id=?", + (json.dumps(merged), symbol_id), + ) diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_index_manager.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_index_manager.py new file mode 100644 index 00000000..e123213f --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_index_manager.py @@ -0,0 +1,354 @@ +""" +SQLite-backed index manager coordinating builder and store. +""" + +from __future__ import annotations + +import json +import logging +import os +import re +import tempfile +import threading +from pathlib import Path +from typing import Any, Dict, List, Optional + +from .sqlite_index_builder import SQLiteIndexBuilder +from .sqlite_store import SQLiteIndexStore, SQLiteSchemaMismatchError +from ..constants import INDEX_FILE_DB, INDEX_FILE, INDEX_FILE_SHALLOW, SETTINGS_DIR + +logger = logging.getLogger(__name__) + + +class SQLiteIndexManager: + """Manage lifecycle of SQLite-backed deep index.""" + + def __init__(self) -> None: + self.project_path: Optional[str] = None + self.index_builder: Optional[SQLiteIndexBuilder] = None + self.store: Optional[SQLiteIndexStore] = None + self.temp_dir: Optional[str] = None + self.index_path: Optional[str] = None + self.shallow_index_path: Optional[str] = None + self._shallow_file_list: Optional[List[str]] = None + self._is_loaded = False + self._lock = threading.RLock() + logger.info("Initialized SQLite Index Manager") + + def set_project_path(self, project_path: str) -> bool: + """Configure project path and underlying storage location.""" + with self._lock: + if not project_path or not isinstance(project_path, str): + logger.error("Invalid project path: %s", project_path) + return False + + project_path = project_path.strip() + if not project_path or not os.path.isdir(project_path): + logger.error("Project path does not exist: %s", project_path) + return False + + self.project_path = project_path + project_hash = _hash_project_path(project_path) + self.temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR, project_hash) + os.makedirs(self.temp_dir, exist_ok=True) + + self.index_path = os.path.join(self.temp_dir, INDEX_FILE_DB) + legacy_path = os.path.join(self.temp_dir, INDEX_FILE) + if os.path.exists(legacy_path): + try: + os.remove(legacy_path) + logger.info("Removed legacy JSON index at %s", legacy_path) + except OSError as exc: # pragma: no cover - best effort + logger.warning("Failed to remove legacy index %s: %s", legacy_path, exc) + + self.shallow_index_path = os.path.join(self.temp_dir, INDEX_FILE_SHALLOW) + self.store = SQLiteIndexStore(self.index_path) + self.index_builder = SQLiteIndexBuilder(project_path, self.store) + self._is_loaded = False + logger.info("SQLite index storage: %s", self.index_path) + return True + + def build_index(self, force_rebuild: bool = False) -> bool: + """Build or rebuild the SQLite index.""" + with self._lock: + if not self.index_builder: + logger.error("Index builder not initialized") + return False + try: + stats = self.index_builder.build_index() + logger.info( + "SQLite index build complete: %s files, %s symbols", + stats.get("files"), + stats.get("symbols"), + ) + self._is_loaded = True + return True + except SQLiteSchemaMismatchError: + logger.warning("Schema mismatch detected; recreating database") + self.store.clear() # type: ignore[union-attr] + stats = self.index_builder.build_index() + logger.info( + "SQLite index rebuild after schema reset: %s files, %s symbols", + stats.get("files"), + stats.get("symbols"), + ) + self._is_loaded = True + return True + except Exception as exc: # pragma: no cover - defensive + logger.error("Failed to build SQLite index: %s", exc) + self._is_loaded = False + return False + + def load_index(self) -> bool: + """Validate that an index database exists and schema is current.""" + with self._lock: + if not self.store: + logger.error("Index store not initialized") + return False + try: + self.store.initialize_schema() + with self.store.connect() as conn: + metadata = self.store.get_metadata(conn, "index_metadata") + except SQLiteSchemaMismatchError: + logger.info("Schema mismatch on load; forcing rebuild on next build_index()") + self._is_loaded = False + return False + except Exception as exc: # pragma: no cover + logger.error("Failed to load SQLite index: %s", exc) + self._is_loaded = False + return False + self._is_loaded = metadata is not None + return self._is_loaded + + def refresh_index(self) -> bool: + """Force rebuild of the SQLite index.""" + with self._lock: + logger.info("Refreshing SQLite deep index...") + if self.build_index(force_rebuild=True): + return self.load_index() + return False + + def build_shallow_index(self) -> bool: + """Build the shallow index file list using existing builder helper.""" + with self._lock: + if not self.index_builder or not self.project_path or not self.shallow_index_path: + logger.error("Index builder not initialized for shallow index") + return False + try: + file_list = self.index_builder.build_shallow_file_list() + with open(self.shallow_index_path, "w", encoding="utf-8") as handle: + json.dump(file_list, handle, ensure_ascii=False) + self._shallow_file_list = file_list + return True + except Exception as exc: # pragma: no cover + logger.error("Failed to build shallow index: %s", exc) + return False + + def load_shallow_index(self) -> bool: + """Load shallow index from disk.""" + with self._lock: + if not self.shallow_index_path or not os.path.exists(self.shallow_index_path): + return False + try: + with open(self.shallow_index_path, "r", encoding="utf-8") as handle: + data = json.load(handle) + if isinstance(data, list): + self._shallow_file_list = [_normalize_path(p) for p in data if isinstance(p, str)] + return True + except Exception as exc: # pragma: no cover + logger.error("Failed to load shallow index: %s", exc) + return False + + def find_files(self, pattern: str = "*") -> List[str]: + """Find files from the shallow index using glob semantics.""" + with self._lock: + if not isinstance(pattern, str): + logger.error("Pattern must be a string, got %s", type(pattern)) + return [] + pattern = pattern.strip() or "*" + norm_pattern = pattern.replace("\\\\", "/").replace("\\", "/") + regex = _compile_glob_regex(norm_pattern) + + if self._shallow_file_list is None: + if not self.load_shallow_index(): + if self.build_shallow_index(): + self.load_shallow_index() + + files = list(self._shallow_file_list or []) + if norm_pattern == "*": + return files + return [f for f in files if regex.match(f)] + + def get_file_summary(self, file_path: str) -> Optional[Dict[str, Any]]: + """Return summary information for a file from SQLite storage.""" + with self._lock: + if not isinstance(file_path, str): + logger.error("File path must be a string, got %s", type(file_path)) + return None + if not self.store or not self._is_loaded: + if not self.load_index(): + return None + + normalized = _normalize_path(file_path) + with self.store.connect() as conn: + row = conn.execute( + """ + SELECT id, language, line_count, imports, exports, docstring + FROM files WHERE path = ? + """, + (normalized,), + ).fetchone() + + if not row: + logger.warning("File not found in index: %s", normalized) + return None + + symbol_rows = conn.execute( + """ + SELECT type, line, signature, docstring, called_by, short_name + FROM symbols + WHERE file_id = ? + ORDER BY line ASC + """, + (row["id"],), + ).fetchall() + + imports = _safe_json_loads(row["imports"]) + exports = _safe_json_loads(row["exports"]) + + categorized = _categorize_symbols(symbol_rows) + + return { + "file_path": normalized, + "language": row["language"], + "line_count": row["line_count"], + "symbol_count": len(symbol_rows), + "functions": categorized["functions"], + "classes": categorized["classes"], + "methods": categorized["methods"], + "imports": imports, + "exports": exports, + "docstring": row["docstring"], + } + + def get_index_stats(self) -> Dict[str, Any]: + """Return basic statistics for the current index.""" + with self._lock: + if not self.store: + return {"status": "not_loaded"} + try: + with self.store.connect() as conn: + metadata = self.store.get_metadata(conn, "index_metadata") + except SQLiteSchemaMismatchError: + return {"status": "not_loaded"} + if not metadata: + return {"status": "not_loaded"} + return { + "status": "loaded" if self._is_loaded else "not_loaded", + "indexed_files": metadata.get("indexed_files", 0), + "total_symbols": metadata.get("total_symbols", 0), + "symbol_types": metadata.get("symbol_types", {}), + "languages": metadata.get("languages", []), + "project_path": metadata.get("project_path"), + "timestamp": metadata.get("timestamp"), + } + + def cleanup(self) -> None: + """Reset internal state.""" + with self._lock: + self.project_path = None + self.index_builder = None + self.store = None + self.temp_dir = None + self.index_path = None + self._shallow_file_list = None + self._is_loaded = False + + +def _hash_project_path(project_path: str) -> str: + import hashlib + + return hashlib.md5(project_path.encode()).hexdigest()[:12] + + +def _compile_glob_regex(pattern: str): + i = 0 + out = [] + special = ".^$+{}[]|()" + while i < len(pattern): + c = pattern[i] + if c == "*": + if i + 1 < len(pattern) and pattern[i + 1] == "*": + out.append(".*") + i += 2 + continue + out.append("[^/]*") + elif c == "?": + out.append("[^/]") + elif c in special: + out.append("\\" + c) + else: + out.append(c) + i += 1 + return re.compile("^" + "".join(out) + "$") + + +def _normalize_path(path: str) -> str: + result = path.replace("\\\\", "/").replace("\\", "/") + if result.startswith("./"): + result = result[2:] + return result + + +def _safe_json_loads(value: Any) -> List[Any]: + if not value: + return [] + if isinstance(value, list): + return value + try: + parsed = json.loads(value) + return parsed if isinstance(parsed, list) else [] + except json.JSONDecodeError: + return [] + + +def _categorize_symbols(symbol_rows) -> Dict[str, List[Dict[str, Any]]]: + functions: List[Dict[str, Any]] = [] + classes: List[Dict[str, Any]] = [] + methods: List[Dict[str, Any]] = [] + + for row in symbol_rows: + symbol_type = row["type"] + called_by = _safe_json_loads(row["called_by"]) + info = { + "name": row["short_name"], + "called_by": called_by, + "line": row["line"], + "signature": row["signature"], + "docstring": row["docstring"], + } + + signature = row["signature"] or "" + if signature.startswith("def ") and "::" in signature: + methods.append(info) + elif signature.startswith("def "): + functions.append(info) + elif signature.startswith("class ") or symbol_type == "class": + classes.append(info) + else: + if symbol_type == "method": + methods.append(info) + elif symbol_type == "class": + classes.append(info) + else: + functions.append(info) + + functions.sort(key=lambda item: item.get("line") or 0) + classes.sort(key=lambda item: item.get("line") or 0) + methods.sort(key=lambda item: item.get("line") or 0) + + return { + "functions": functions, + "classes": classes, + "methods": methods, + } diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_store.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_store.py new file mode 100644 index 00000000..5b7e6e6b --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/sqlite_store.py @@ -0,0 +1,173 @@ +""" +SQLite storage layer for deep code index data. + +This module centralizes SQLite setup, schema management, and connection +pragmas so higher-level builders/managers can focus on data orchestration. +""" + +from __future__ import annotations + +import json +import os +import sqlite3 +import threading +from contextlib import contextmanager +from typing import Any, Dict, Generator, Optional + +SCHEMA_VERSION = 1 + + +class SQLiteSchemaMismatchError(RuntimeError): + """Raised when the on-disk schema cannot be used safely.""" + + +class SQLiteIndexStore: + """Utility wrapper around an on-disk SQLite database for the deep index.""" + + def __init__(self, db_path: str) -> None: + if not db_path or not isinstance(db_path, str): + raise ValueError("db_path must be a non-empty string") + self.db_path = db_path + self._lock = threading.RLock() + + def initialize_schema(self) -> None: + """Create database schema if needed and validate schema version.""" + with self._lock: + os.makedirs(os.path.dirname(self.db_path), exist_ok=True) + with self.connect(for_build=True) as conn: + self._create_tables(conn) + self._ensure_schema_version(conn) + # Ensure metadata contains the canonical project path placeholder + if self.get_metadata(conn, "project_path") is None: + self.set_metadata(conn, "project_path", "") + + @contextmanager + def connect(self, *, for_build: bool = False) -> Generator[sqlite3.Connection, None, None]: + """ + Context manager yielding a configured SQLite connection. + + Args: + for_build: Apply write-optimized pragmas (journal mode, cache size). + """ + with self._lock: + conn = sqlite3.connect(self.db_path, check_same_thread=False) + conn.row_factory = sqlite3.Row + self._apply_pragmas(conn, for_build) + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + conn.close() + + def clear(self) -> None: + """Remove existing database file.""" + with self._lock: + if os.path.exists(self.db_path): + os.remove(self.db_path) + + # Metadata helpers ------------------------------------------------- + + def set_metadata(self, conn: sqlite3.Connection, key: str, value: Any) -> None: + """Persist a metadata key/value pair (value stored as JSON string).""" + conn.execute( + """ + INSERT INTO metadata(key, value) + VALUES(?, ?) + ON CONFLICT(key) DO UPDATE SET value=excluded.value + """, + (key, json.dumps(value)), + ) + + def get_metadata(self, conn: sqlite3.Connection, key: str) -> Optional[Any]: + """Retrieve a metadata value (deserialized from JSON).""" + row = conn.execute("SELECT value FROM metadata WHERE key=?", (key,)).fetchone() + if not row: + return None + try: + return json.loads(row["value"]) + except json.JSONDecodeError: + return row["value"] + + # Internal helpers ------------------------------------------------- + + def _create_tables(self, conn: sqlite3.Connection) -> None: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY, + path TEXT UNIQUE NOT NULL, + language TEXT, + line_count INTEGER, + imports TEXT, + exports TEXT, + package TEXT, + docstring TEXT + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS symbols ( + id INTEGER PRIMARY KEY, + symbol_id TEXT UNIQUE NOT NULL, + file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, + type TEXT, + line INTEGER, + signature TEXT, + docstring TEXT, + called_by TEXT, + short_name TEXT + ) + """ + ) + conn.execute( + """ + CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id) + """ + ) + conn.execute( + """ + CREATE INDEX IF NOT EXISTS idx_symbols_short_name ON symbols(short_name) + """ + ) + + def _ensure_schema_version(self, conn: sqlite3.Connection) -> None: + stored = self.get_metadata(conn, "schema_version") + if stored is None: + self.set_metadata(conn, "schema_version", SCHEMA_VERSION) + return + + if int(stored) != SCHEMA_VERSION: + raise SQLiteSchemaMismatchError( + f"Unexpected schema version {stored} (expected {SCHEMA_VERSION})" + ) + + def _apply_pragmas(self, conn: sqlite3.Connection, for_build: bool) -> None: + pragmas: Dict[str, Any] = { + "journal_mode": "WAL" if for_build else "WAL", + "synchronous": "NORMAL" if for_build else "FULL", + "cache_size": -262144, # negative => size in KB, ~256MB + } + for pragma, value in pragmas.items(): + try: + conn.execute(f"PRAGMA {pragma}={value}") + except sqlite3.DatabaseError: + # PRAGMA not supported or rejected; continue best-effort. + continue + if for_build: + try: + conn.execute("PRAGMA temp_store=MEMORY") + except sqlite3.DatabaseError: + pass + diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/__init__.py new file mode 100644 index 00000000..0f512742 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/__init__.py @@ -0,0 +1,8 @@ +""" +Parsing strategies for different programming languages. +""" + +from .base_strategy import ParsingStrategy +from .strategy_factory import StrategyFactory + +__all__ = ['ParsingStrategy', 'StrategyFactory'] \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/base_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/base_strategy.py new file mode 100644 index 00000000..6ef5b4f1 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/base_strategy.py @@ -0,0 +1,91 @@ +""" +Abstract base class for language parsing strategies. +""" + +import os +from abc import ABC, abstractmethod +from typing import Dict, List, Tuple, Optional +from ..models import SymbolInfo, FileInfo + + +class ParsingStrategy(ABC): + """Abstract base class for language parsing strategies.""" + + @abstractmethod + def get_language_name(self) -> str: + """Return the language name this strategy handles.""" + + @abstractmethod + def get_supported_extensions(self) -> List[str]: + """Return list of file extensions this strategy supports.""" + + @abstractmethod + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """ + Parse file content and extract symbols. + + Args: + file_path: Path to the file being parsed + content: File content as string + + Returns: + Tuple of (symbols_dict, file_info) + - symbols_dict: Maps symbol_id -> SymbolInfo + - file_info: FileInfo with metadata about the file + """ + + def _create_symbol_id(self, file_path: str, symbol_name: str) -> str: + """ + Create a unique symbol ID. + + Args: + file_path: Path to the file containing the symbol + symbol_name: Name of the symbol + + Returns: + Unique symbol identifier in format "relative_path::symbol_name" + """ + relative_path = self._get_relative_path(file_path) + return f"{relative_path}::{symbol_name}" + + def _get_relative_path(self, file_path: str) -> str: + """Normalize path for symbol identifiers relative to project root.""" + if not file_path: + return "" + + normalized = os.path.normpath(file_path) + if normalized == ".": + return "" + + normalized = normalized.replace("\\", "/") + if normalized.startswith("./"): + normalized = normalized[2:] + + if not os.path.isabs(file_path): + normalized = normalized.lstrip("/") + + return normalized or os.path.basename(file_path) + + def _extract_line_number(self, content: str, symbol_position: int) -> int: + """ + Extract line number from character position in content. + + Args: + content: File content + symbol_position: Character position in content + + Returns: + Line number (1-based) + """ + return content[:symbol_position].count('\n') + 1 + + def _get_file_name(self, file_path: str) -> str: + """Get just the filename from a full path.""" + return os.path.basename(file_path) + + def _safe_extract_text(self, content: str, start: int, end: int) -> str: + """Safely extract text from content, handling bounds.""" + try: + return content[start:end].strip() + except (IndexError, TypeError): + return "" diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/fallback_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/fallback_strategy.py new file mode 100644 index 00000000..21653bd9 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/fallback_strategy.py @@ -0,0 +1,46 @@ +""" +Fallback parsing strategy for unsupported languages and file types. +""" + +import os +from typing import Dict, List, Tuple +from .base_strategy import ParsingStrategy +from ..models import SymbolInfo, FileInfo + + +class FallbackParsingStrategy(ParsingStrategy): + """Fallback parser for unsupported languages and file types.""" + + def __init__(self, language_name: str = "unknown"): + self.language_name = language_name + + def get_language_name(self) -> str: + return self.language_name + + def get_supported_extensions(self) -> List[str]: + return [] # Fallback supports any extension + + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Basic parsing: extract file information without symbol parsing.""" + symbols = {} + + # For document files, we can at least index their existence + file_info = FileInfo( + language=self.language_name, + line_count=len(content.splitlines()), + symbols={"functions": [], "classes": []}, + imports=[] + ) + + # For document files (e.g. .md, .txt, .json), we can add a symbol representing the file itself + if self.language_name in ['markdown', 'text', 'json', 'yaml', 'xml', 'config', 'css', 'html']: + filename = os.path.basename(file_path) + symbol_id = self._create_symbol_id(file_path, f"file:{filename}") + symbols[symbol_id] = SymbolInfo( + type="file", + file=file_path, + line=1, + signature=f"{self.language_name} file: {filename}" + ) + + return symbols, file_info diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/go_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/go_strategy.py new file mode 100644 index 00000000..77b454fd --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/go_strategy.py @@ -0,0 +1,359 @@ +""" +Go parsing strategy using regex patterns. +""" + +import re +from typing import Dict, List, Tuple, Optional +from .base_strategy import ParsingStrategy +from ..models import SymbolInfo, FileInfo + + +class GoParsingStrategy(ParsingStrategy): + """Go-specific parsing strategy using regex patterns.""" + + def get_language_name(self) -> str: + return "go" + + def get_supported_extensions(self) -> List[str]: + return ['.go'] + + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Parse Go file using regex patterns.""" + symbols = {} + functions = [] + lines = content.splitlines() + classes = [] # Go doesn't have classes, but we'll track structs/interfaces + imports = self._extract_go_imports(lines) + package = None + + for i, line in enumerate(lines): + line = line.strip() + + # Package declaration + if line.startswith('package '): + package = line.split('package ')[1].strip() + + # Function declarations + elif line.startswith('func '): + func_match = re.match(r'func\s+(\w+)\s*\(', line) + if func_match: + func_name = func_match.group(1) + docstring = self._extract_go_comment(lines, i) + symbol_id = self._create_symbol_id(file_path, func_name) + symbols[symbol_id] = SymbolInfo( + type="function", + file=file_path, + line=i + 1, + signature=line, + docstring=docstring + ) + functions.append(func_name) + + # Method declarations (func (receiver) methodName) + method_match = re.match(r'func\s+\([^)]+\)\s+(\w+)\s*\(', line) + if method_match: + method_name = method_match.group(1) + docstring = self._extract_go_comment(lines, i) + symbol_id = self._create_symbol_id(file_path, method_name) + symbols[symbol_id] = SymbolInfo( + type="method", + file=file_path, + line=i + 1, + signature=line, + docstring=docstring + ) + functions.append(method_name) + + # Struct declarations + elif re.match(r'type\s+\w+\s+struct\s*\{', line): + struct_match = re.match(r'type\s+(\w+)\s+struct', line) + if struct_match: + struct_name = struct_match.group(1) + docstring = self._extract_go_comment(lines, i) + symbol_id = self._create_symbol_id(file_path, struct_name) + symbols[symbol_id] = SymbolInfo( + type="struct", + file=file_path, + line=i + 1, + docstring=docstring + ) + classes.append(struct_name) + + # Interface declarations + elif re.match(r'type\s+\w+\s+interface\s*\{', line): + interface_match = re.match(r'type\s+(\w+)\s+interface', line) + if interface_match: + interface_name = interface_match.group(1) + docstring = self._extract_go_comment(lines, i) + symbol_id = self._create_symbol_id(file_path, interface_name) + symbols[symbol_id] = SymbolInfo( + type="interface", + file=file_path, + line=i + 1, + docstring=docstring + ) + classes.append(interface_name) + + # Phase 2: Add call relationship analysis + self._analyze_go_calls(content, symbols, file_path) + + file_info = FileInfo( + language=self.get_language_name(), + line_count=len(lines), + symbols={"functions": functions, "classes": classes}, + imports=imports, + package=package + ) + + return symbols, file_info + + def _analyze_go_calls(self, content: str, symbols: Dict[str, SymbolInfo], file_path: str): + """Analyze Go function calls for relationships.""" + lines = content.splitlines() + current_function = None + is_function_declaration_line = False + + for i, line in enumerate(lines): + original_line = line + line = line.strip() + + # Track current function context + if line.startswith('func '): + func_name = self._extract_go_function_name(line) + if func_name: + current_function = self._create_symbol_id(file_path, func_name) + is_function_declaration_line = True + else: + is_function_declaration_line = False + + # Find function calls: functionName() or obj.methodName() + # Skip the function declaration line itself to avoid false self-calls + if current_function and not is_function_declaration_line and ('(' in line and ')' in line): + called_functions = self._extract_go_called_functions(line) + for called_func in called_functions: + # Find the called function in symbols and add relationship + for symbol_id, symbol_info in symbols.items(): + if called_func in symbol_id.split("::")[-1]: + if current_function not in symbol_info.called_by: + symbol_info.called_by.append(current_function) + + def _extract_go_function_name(self, line: str) -> Optional[str]: + """Extract function name from Go function declaration.""" + try: + # func functionName(...) or func (receiver) methodName(...) + match = re.match(r'func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(', line) + if match: + return match.group(1) + except: + pass + return None + + def _extract_go_imports(self, lines: List[str]) -> List[str]: + """Extract Go import paths, handling multi-line blocks and comments.""" + imports: List[str] = [] + in_block_comment = False + paren_depth = 0 + + for raw_line in lines: + clean_line, in_block_comment = self._strip_go_comments(raw_line, in_block_comment) + stripped = clean_line.strip() + + if not stripped: + continue + + if paren_depth == 0: + if not stripped.startswith('import '): + continue + + remainder = stripped[len('import '):].strip() + if not remainder: + continue + + imports.extend(self._extract_string_literals(remainder)) + + paren_depth = ( + self._count_unquoted_characters(remainder, '(') + - self._count_unquoted_characters(remainder, ')') + ) + if paren_depth <= 0: + paren_depth = 0 + continue + + imports.extend(self._extract_string_literals(clean_line)) + paren_depth += self._count_unquoted_characters(clean_line, '(') + paren_depth -= self._count_unquoted_characters(clean_line, ')') + if paren_depth <= 0: + paren_depth = 0 + + return imports + + def _strip_go_comments(self, line: str, in_block_comment: bool) -> Tuple[str, bool]: + """Remove Go comments from a line while tracking block comment state.""" + result: List[str] = [] + i = 0 + length = len(line) + + while i < length: + if in_block_comment: + if line.startswith('*/', i): + in_block_comment = False + i += 2 + else: + i += 1 + continue + + if line.startswith('//', i): + break + + if line.startswith('/*', i): + in_block_comment = True + i += 2 + continue + + result.append(line[i]) + i += 1 + + return ''.join(result), in_block_comment + + def _extract_string_literals(self, line: str) -> List[str]: + """Return string literal values found in a line (supports " and `).""" + literals: List[str] = [] + i = 0 + length = len(line) + + while i < length: + char = line[i] + if char not in ('"', '`'): + i += 1 + continue + + delimiter = char + i += 1 + buffer: List[str] = [] + while i < length: + current = line[i] + if delimiter == '"': + if current == '\\': + if i + 1 < length: + buffer.append(line[i + 1]) + i += 2 + continue + elif current == '"': + literals.append(''.join(buffer)) + i += 1 + break + else: # Raw string delimited by backticks + if current == '`': + literals.append(''.join(buffer)) + i += 1 + break + + buffer.append(current) + i += 1 + else: + break + + return literals + + def _count_unquoted_characters(self, line: str, target: str) -> int: + """Count occurrences of a character outside string literals.""" + count = 0 + i = 0 + length = len(line) + delimiter: Optional[str] = None + + while i < length: + char = line[i] + if delimiter is None: + if char in ('"', '`'): + delimiter = char + elif char == target: + count += 1 + else: + if delimiter == '"': + if char == '\\': + i += 2 + continue + if char == '"': + delimiter = None + elif delimiter == '`' and char == '`': + delimiter = None + + i += 1 + + return count + + def _extract_go_comment(self, lines: List[str], line_index: int) -> Optional[str]: + """Extract Go comment (docstring) from lines preceding the given line. + + Go documentation comments are regular comments that appear immediately before + the declaration, with no blank line in between. + """ + comment_lines = [] + + # Look backwards from the line before the declaration + i = line_index - 1 + while i >= 0: + stripped = lines[i].strip() + + # Stop at empty line + if not stripped: + break + + # Single-line comment + if stripped.startswith('//'): + comment_text = stripped[2:].strip() + comment_lines.insert(0, comment_text) + i -= 1 + # Multi-line comment block + elif stripped.startswith('/*') or stripped.endswith('*/'): + # Handle single-line /* comment */ + if stripped.startswith('/*') and stripped.endswith('*/'): + comment_text = stripped[2:-2].strip() + comment_lines.insert(0, comment_text) + i -= 1 + # Handle multi-line comment block + elif stripped.endswith('*/'): + # Found end of multi-line comment, collect until start + temp_lines = [] + temp_lines.insert(0, stripped[:-2].strip()) + i -= 1 + while i >= 0: + temp_stripped = lines[i].strip() + if temp_stripped.startswith('/*'): + temp_lines.insert(0, temp_stripped[2:].strip()) + comment_lines = temp_lines + comment_lines + i -= 1 + break + else: + temp_lines.insert(0, temp_stripped) + i -= 1 + break + else: + break + else: + # Not a comment, stop looking + break + + if comment_lines: + # Join with newlines and clean up + docstring = '\n'.join(comment_lines) + return docstring if docstring else None + + return None + + def _extract_go_called_functions(self, line: str) -> List[str]: + """Extract function names that are being called in this line.""" + called_functions = [] + + # Find patterns like: functionName( or obj.methodName( + patterns = [ + r'(\w+)\s*\(', # functionName( + r'\.(\w+)\s*\(', # .methodName( + ] + + for pattern in patterns: + matches = re.findall(pattern, line) + called_functions.extend(matches) + + return called_functions diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/java_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/java_strategy.py new file mode 100644 index 00000000..af2ff8e7 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/java_strategy.py @@ -0,0 +1,209 @@ +""" +Java parsing strategy using tree-sitter - Optimized single-pass version. +""" + +import logging +from typing import Dict, List, Tuple, Optional, Set +from .base_strategy import ParsingStrategy +from ..models import SymbolInfo, FileInfo + +logger = logging.getLogger(__name__) + +import tree_sitter +from tree_sitter_java import language + + +class JavaParsingStrategy(ParsingStrategy): + """Java-specific parsing strategy - Single Pass Optimized.""" + + def __init__(self): + self.java_language = tree_sitter.Language(language()) + + def get_language_name(self) -> str: + return "java" + + def get_supported_extensions(self) -> List[str]: + return ['.java'] + + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Parse Java file using tree-sitter with single-pass optimization.""" + symbols = {} + functions = [] + classes = [] + imports = [] + package = None + + # Symbol lookup index for O(1) access + symbol_lookup = {} # name -> symbol_id mapping + + parser = tree_sitter.Parser(self.java_language) + + try: + tree = parser.parse(content.encode('utf8')) + + # Extract package info first + for node in tree.root_node.children: + if node.type == 'package_declaration': + package = self._extract_java_package(node, content) + break + + # Single-pass traversal that handles everything + context = TraversalContext( + content=content, + file_path=file_path, + symbols=symbols, + functions=functions, + classes=classes, + imports=imports, + symbol_lookup=symbol_lookup + ) + + self._traverse_node_single_pass(tree.root_node, context) + + except Exception as e: + logger.warning(f"Error parsing Java file {file_path}: {e}") + + file_info = FileInfo( + language=self.get_language_name(), + line_count=len(content.splitlines()), + symbols={"functions": functions, "classes": classes}, + imports=imports, + package=package + ) + + return symbols, file_info + + def _traverse_node_single_pass(self, node, context: 'TraversalContext', + current_class: Optional[str] = None, + current_method: Optional[str] = None): + """Single-pass traversal that extracts symbols and analyzes calls.""" + + # Handle class declarations + if node.type == 'class_declaration': + name = self._get_java_class_name(node, context.content) + if name: + symbol_id = self._create_symbol_id(context.file_path, name) + symbol_info = SymbolInfo( + type="class", + file=context.file_path, + line=node.start_point[0] + 1 + ) + context.symbols[symbol_id] = symbol_info + context.symbol_lookup[name] = symbol_id + context.classes.append(name) + + # Traverse class body with updated context + for child in node.children: + self._traverse_node_single_pass(child, context, current_class=name, current_method=current_method) + return + + # Handle method declarations + elif node.type == 'method_declaration': + name = self._get_java_method_name(node, context.content) + if name: + # Build full method name with class context + if current_class: + full_name = f"{current_class}.{name}" + else: + full_name = name + + symbol_id = self._create_symbol_id(context.file_path, full_name) + symbol_info = SymbolInfo( + type="method", + file=context.file_path, + line=node.start_point[0] + 1, + signature=self._get_java_method_signature(node, context.content) + ) + context.symbols[symbol_id] = symbol_info + context.symbol_lookup[full_name] = symbol_id + context.symbol_lookup[name] = symbol_id # Also index by method name alone + context.functions.append(full_name) + + # Traverse method body with updated context + for child in node.children: + self._traverse_node_single_pass(child, context, current_class=current_class, + current_method=symbol_id) + return + + # Handle method invocations (calls) + elif node.type == 'method_invocation': + if current_method: + called_method = self._get_called_method_name(node, context.content) + if called_method: + # Use O(1) lookup instead of O(n) iteration + if called_method in context.symbol_lookup: + symbol_id = context.symbol_lookup[called_method] + symbol_info = context.symbols[symbol_id] + if current_method not in symbol_info.called_by: + symbol_info.called_by.append(current_method) + else: + # Try to find method with class prefix + for name, sid in context.symbol_lookup.items(): + if name.endswith(f".{called_method}"): + symbol_info = context.symbols[sid] + if current_method not in symbol_info.called_by: + symbol_info.called_by.append(current_method) + break + + # Handle import declarations + elif node.type == 'import_declaration': + import_text = context.content[node.start_byte:node.end_byte] + # Extract the import path (remove 'import' keyword and semicolon) + import_path = import_text.replace('import', '').replace(';', '').strip() + if import_path: + context.imports.append(import_path) + + # Continue traversing children for other node types + for child in node.children: + self._traverse_node_single_pass(child, context, current_class=current_class, + current_method=current_method) + + def _get_java_class_name(self, node, content: str) -> Optional[str]: + for child in node.children: + if child.type == 'identifier': + return content[child.start_byte:child.end_byte] + return None + + def _get_java_method_name(self, node, content: str) -> Optional[str]: + for child in node.children: + if child.type == 'identifier': + return content[child.start_byte:child.end_byte] + return None + + def _get_java_method_signature(self, node, content: str) -> str: + return content[node.start_byte:node.end_byte].split('\n')[0].strip() + + def _extract_java_package(self, node, content: str) -> Optional[str]: + for child in node.children: + if child.type == 'scoped_identifier': + return content[child.start_byte:child.end_byte] + return None + + def _get_called_method_name(self, node, content: str) -> Optional[str]: + """Extract called method name from method invocation node.""" + # Handle obj.method() pattern - look for the method name after the dot + for child in node.children: + if child.type == 'field_access': + # For field_access nodes, get the field (method) name + for subchild in child.children: + if subchild.type == 'identifier' and subchild.start_byte > child.start_byte: + # Get the rightmost identifier (the method name) + return content[subchild.start_byte:subchild.end_byte] + elif child.type == 'identifier': + # Direct method call without object reference + return content[child.start_byte:child.end_byte] + return None + + +class TraversalContext: + """Context object to pass state during single-pass traversal.""" + + def __init__(self, content: str, file_path: str, symbols: Dict, + functions: List, classes: List, imports: List, symbol_lookup: Dict): + self.content = content + self.file_path = file_path + self.symbols = symbols + self.functions = functions + self.classes = classes + self.imports = imports + self.symbol_lookup = symbol_lookup \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/javascript_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/javascript_strategy.py new file mode 100644 index 00000000..3994dabb --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/javascript_strategy.py @@ -0,0 +1,628 @@ +""" +JavaScript parsing strategy using tree-sitter. +""" + +import logging +from typing import Dict, List, Tuple, Optional, Set + +import tree_sitter +from tree_sitter_javascript import language + +from .base_strategy import ParsingStrategy +from ..models import SymbolInfo, FileInfo + +logger = logging.getLogger(__name__) + + +class JavaScriptParsingStrategy(ParsingStrategy): + """JavaScript-specific parsing strategy using tree-sitter.""" + + def __init__(self): + self.js_language = tree_sitter.Language(language()) + + def get_language_name(self) -> str: + return "javascript" + + def get_supported_extensions(self) -> List[str]: + return ['.js', '.jsx', '.mjs', '.cjs'] + + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Parse JavaScript file using tree-sitter.""" + symbols: Dict[str, SymbolInfo] = {} + functions: List[str] = [] + classes: List[str] = [] + imports: List[str] = [] + exports: List[str] = [] + symbol_lookup: Dict[str, str] = {} + pending_calls: List[Tuple[str, str]] = [] + pending_call_set: Set[Tuple[str, str]] = set() + variable_scopes: List[Dict[str, str]] = [{}] + + parser = tree_sitter.Parser(self.js_language) + tree = parser.parse(content.encode('utf8')) + self._traverse_js_node( + tree.root_node, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + ) + + file_info = FileInfo( + language=self.get_language_name(), + line_count=len(content.splitlines()), + symbols={"functions": functions, "classes": classes}, + imports=imports, + exports=exports + ) + + if pending_calls: + file_info.pending_calls = pending_calls + + return symbols, file_info + + def _traverse_js_node( + self, + node, + content: str, + file_path: str, + symbols: Dict[str, SymbolInfo], + functions: List[str], + classes: List[str], + imports: List[str], + exports: List[str], + symbol_lookup: Dict[str, str], + pending_calls: List[Tuple[str, str]], + pending_call_set: Set[Tuple[str, str]], + variable_scopes: List[Dict[str, str]], + current_function: Optional[str] = None, + current_class: Optional[str] = None, + ): + """Traverse JavaScript AST node and collect symbols and relationships.""" + node_type = node.type + + if node_type == 'function_declaration': + name = self._get_function_name(node, content) + if name: + symbol_id = self._create_symbol_id(file_path, name) + signature = self._get_js_function_signature(node, content) + symbols[symbol_id] = SymbolInfo( + type="function", + file=file_path, + line=node.start_point[0] + 1, + signature=signature + ) + symbol_lookup[name] = symbol_id + functions.append(name) + function_id = f"{file_path}::{name}" + variable_scopes.append({}) + for child in node.children: + self._traverse_js_node( + child, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_function=function_id, + current_class=current_class, + ) + variable_scopes.pop() + return + + if node_type == 'class_declaration': + name = self._get_class_name(node, content) + if name: + symbol_id = self._create_symbol_id(file_path, name) + symbols[symbol_id] = SymbolInfo( + type="class", + file=file_path, + line=node.start_point[0] + 1 + ) + symbol_lookup[name] = symbol_id + classes.append(name) + for child in node.children: + self._traverse_js_node( + child, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_function=current_function, + current_class=name, + ) + return + + if node_type == 'method_definition': + method_name = self._get_method_name(node, content) + class_name = current_class or self._find_parent_class(node, content) + if method_name and class_name: + full_name = f"{class_name}.{method_name}" + symbol_id = self._create_symbol_id(file_path, full_name) + signature = self._get_js_function_signature(node, content) + symbols[symbol_id] = SymbolInfo( + type="method", + file=file_path, + line=node.start_point[0] + 1, + signature=signature + ) + symbol_lookup[full_name] = symbol_id + symbol_lookup[method_name] = symbol_id + functions.append(full_name) + function_id = f"{file_path}::{full_name}" + variable_scopes.append({}) + for child in node.children: + self._traverse_js_node( + child, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_function=function_id, + current_class=class_name, + ) + variable_scopes.pop() + return + + if node_type in ['lexical_declaration', 'variable_declaration']: + for child in node.children: + if child.type != 'variable_declarator': + self._traverse_js_node( + child, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_function=current_function, + current_class=current_class, + ) + continue + + name_node = child.child_by_field_name('name') + value_node = child.child_by_field_name('value') + if not name_node: + continue + + name = self._get_node_text(name_node, content) + + if value_node and value_node.type in ['arrow_function', 'function_expression', 'function']: + symbol_id = self._create_symbol_id(file_path, name) + signature = content[child.start_byte:child.end_byte].split('\n')[0].strip() + symbols[symbol_id] = SymbolInfo( + type="function", + file=file_path, + line=child.start_point[0] + 1, + signature=signature + ) + symbol_lookup[name] = symbol_id + functions.append(name) + function_id = f"{file_path}::{name}" + variable_scopes.append({}) + self._traverse_js_node( + value_node, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_function=function_id, + current_class=current_class, + ) + variable_scopes.pop() + else: + inferred = self._infer_expression_type(value_node, content) + if inferred: + self._set_variable_type(variable_scopes, name, inferred) + if value_node: + self._traverse_js_node( + value_node, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_function=current_function, + current_class=current_class, + ) + return + + if node_type == 'arrow_function': + variable_scopes.append({}) + for child in node.children: + self._traverse_js_node( + child, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_function=current_function, + current_class=current_class, + ) + variable_scopes.pop() + return + + if node_type == 'call_expression': + caller = current_function or f"{file_path}:{node.start_point[0] + 1}" + called = self._resolve_called_function( + node, + content, + variable_scopes, + current_class + ) + if caller and called: + self._register_call( + symbols, + symbol_lookup, + pending_calls, + pending_call_set, + caller, + called + ) + if caller: + self._collect_callback_arguments( + node, + content, + symbols, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_class, + caller + ) + + if node_type in ['import_statement', 'require_call']: + import_text = self._get_node_text(node, content) + imports.append(import_text) + elif node_type in ['export_statement', 'export_clause', 'export_default_declaration']: + exports.append(self._get_node_text(node, content)) + + for child in node.children: + self._traverse_js_node( + child, + content, + file_path, + symbols, + functions, + classes, + imports, + exports, + symbol_lookup, + pending_calls, + pending_call_set, + variable_scopes, + current_function=current_function, + current_class=current_class, + ) + + def _collect_callback_arguments( + self, + call_node, + content: str, + symbols: Dict[str, SymbolInfo], + symbol_lookup: Dict[str, str], + pending_calls: List[Tuple[str, str]], + pending_call_set: Set[Tuple[str, str]], + variable_scopes: List[Dict[str, str]], + current_class: Optional[str], + caller: str + ) -> None: + """Capture identifier callbacks passed as call expression arguments.""" + arguments_node = call_node.child_by_field_name('arguments') + if not arguments_node: + return + + for argument in arguments_node.children: + if not getattr(argument, "is_named", False): + continue + callback_name = self._resolve_argument_reference( + argument, + content, + variable_scopes, + current_class + ) + if not callback_name: + continue + self._register_call( + symbols, + symbol_lookup, + pending_calls, + pending_call_set, + caller, + callback_name + ) + + def _resolve_argument_reference( + self, + node, + content: str, + variable_scopes: List[Dict[str, str]], + current_class: Optional[str] + ) -> Optional[str]: + """Resolve a potential callback reference used as an argument.""" + node_type = node.type + + if node_type == 'identifier': + return self._get_node_text(node, content) + + if node_type == 'member_expression': + property_node = node.child_by_field_name('property') + if property_node is None: + for child in node.children: + if child.type in ['property_identifier', 'identifier']: + property_node = child + break + if property_node is None: + return None + + property_name = self._get_node_text(property_node, content) + qualifier_node = node.child_by_field_name('object') + qualifier = None + if qualifier_node is not None: + qualifier = self._resolve_member_qualifier( + qualifier_node, + content, + variable_scopes, + current_class + ) + if not qualifier: + for child in node.children: + if child is property_node: + continue + qualifier = self._resolve_member_qualifier( + child, + content, + variable_scopes, + current_class + ) + if qualifier: + break + if qualifier: + return f"{qualifier}.{property_name}" + return property_name + + if node_type in ['call_expression', 'arrow_function', 'function', 'function_expression']: + return None + + return None + + def _get_function_name(self, node, content: str) -> Optional[str]: + """Extract function name from tree-sitter node.""" + for child in node.children: + if child.type == 'identifier': + return self._get_node_text(child, content) + return None + + def _get_class_name(self, node, content: str) -> Optional[str]: + """Extract class name from tree-sitter node.""" + for child in node.children: + if child.type == 'identifier': + return self._get_node_text(child, content) + return None + + def _get_method_name(self, node, content: str) -> Optional[str]: + """Extract method name from tree-sitter node.""" + for child in node.children: + if child.type == 'property_identifier': + return self._get_node_text(child, content) + return None + + def _find_parent_class(self, node, content: str) -> Optional[str]: + """Find the parent class of a method.""" + parent = node.parent + while parent: + if parent.type == 'class_declaration': + return self._get_class_name(parent, content) + parent = parent.parent + return None + + def _get_js_function_signature(self, node, content: str) -> str: + """Extract JavaScript function signature.""" + return content[node.start_byte:node.end_byte].split('\n')[0].strip() + + def _get_node_text(self, node, content: str) -> str: + return content[node.start_byte:node.end_byte] + + def _set_variable_type(self, variable_scopes: List[Dict[str, str]], name: str, value: str) -> None: + if not variable_scopes: + return + variable_scopes[-1][name] = value + + def _lookup_variable_type(self, variable_scopes: List[Dict[str, str]], name: str) -> Optional[str]: + for scope in reversed(variable_scopes): + if name in scope: + return scope[name] + return None + + def _infer_expression_type(self, node, content: str) -> Optional[str]: + """Infer the class/type from a simple expression like `new ClassName()`.""" + if node is None: + return None + + if node.type == 'new_expression': + constructor_node = node.child_by_field_name('constructor') + if constructor_node is None: + # Fallback: first identifier or member expression child + for child in node.children: + if child.type in ['identifier', 'member_expression']: + constructor_node = child + break + + if constructor_node: + if constructor_node.type == 'identifier': + return self._get_node_text(constructor_node, content) + if constructor_node.type == 'member_expression': + property_node = constructor_node.child_by_field_name('property') + if property_node: + return self._get_node_text(property_node, content) + for child in reversed(constructor_node.children): + if child.type in ['identifier', 'property_identifier']: + return self._get_node_text(child, content) + return None + + def _resolve_called_function( + self, + node, + content: str, + variable_scopes: List[Dict[str, str]], + current_class: Optional[str] + ) -> Optional[str]: + function_node = node.child_by_field_name('function') + if function_node is None and node.children: + function_node = node.children[0] + if function_node is None: + return None + + if function_node.type == 'identifier': + return self._get_node_text(function_node, content) + + if function_node.type == 'member_expression': + property_node = function_node.child_by_field_name('property') + if property_node is None: + for child in function_node.children: + if child.type in ['property_identifier', 'identifier']: + property_node = child + break + if property_node is None: + return None + + property_name = self._get_node_text(property_node, content) + object_node = function_node.child_by_field_name('object') + qualifier = None + if object_node is not None: + qualifier = self._resolve_member_qualifier( + object_node, + content, + variable_scopes, + current_class + ) + else: + for child in function_node.children: + if child is property_node: + continue + qualifier = self._resolve_member_qualifier( + child, + content, + variable_scopes, + current_class + ) + if qualifier: + break + + if qualifier: + return f"{qualifier}.{property_name}" + return property_name + + return None + + def _resolve_member_qualifier( + self, + node, + content: str, + variable_scopes: List[Dict[str, str]], + current_class: Optional[str] + ) -> Optional[str]: + node_type = node.type + if node_type == 'this': + return current_class + + if node_type == 'identifier': + name = self._get_node_text(node, content) + var_type = self._lookup_variable_type(variable_scopes, name) + return var_type or name + + if node_type == 'member_expression': + property_node = node.child_by_field_name('property') + if property_node is None: + for child in node.children: + if child.type in ['property_identifier', 'identifier']: + property_node = child + break + if property_node is None: + return None + + qualifier = self._resolve_member_qualifier( + node.child_by_field_name('object'), + content, + variable_scopes, + current_class + ) + property_name = self._get_node_text(property_node, content) + if qualifier: + return f"{qualifier}.{property_name}" + return property_name + + return None + + def _register_call( + self, + symbols: Dict[str, SymbolInfo], + symbol_lookup: Dict[str, str], + pending_calls: List[Tuple[str, str]], + pending_call_set: Set[Tuple[str, str]], + caller: str, + called: str + ) -> None: + if called in symbol_lookup: + symbol_info = symbols[symbol_lookup[called]] + if caller not in symbol_info.called_by: + symbol_info.called_by.append(caller) + return + + key = (caller, called) + if key not in pending_call_set: + pending_call_set.add(key) + pending_calls.append(key) diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/objective_c_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/objective_c_strategy.py new file mode 100644 index 00000000..4226f1cc --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/objective_c_strategy.py @@ -0,0 +1,154 @@ +""" +Objective-C parsing strategy using regex patterns. +""" + +import re +from typing import Dict, List, Tuple, Optional +from .base_strategy import ParsingStrategy +from ..models import SymbolInfo, FileInfo + + +class ObjectiveCParsingStrategy(ParsingStrategy): + """Objective-C parsing strategy using regex patterns.""" + + def get_language_name(self) -> str: + return "objective-c" + + def get_supported_extensions(self) -> List[str]: + return ['.m', '.mm'] + + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Parse Objective-C file using regex patterns.""" + symbols = {} + functions = [] + classes = [] + imports = [] + + lines = content.splitlines() + current_class = None + + for i, line in enumerate(lines): + line = line.strip() + + # Import statements + if line.startswith('#import ') or line.startswith('#include '): + import_match = re.search(r'#(?:import|include)\s+[<"]([^>"]+)[>"]', line) + if import_match: + imports.append(import_match.group(1)) + + # Interface declarations + elif line.startswith('@interface '): + interface_match = re.match(r'@interface\s+(\w+)', line) + if interface_match: + class_name = interface_match.group(1) + current_class = class_name + symbol_id = self._create_symbol_id(file_path, class_name) + symbols[symbol_id] = SymbolInfo( + type="class", + file=file_path, + line=i + 1 + ) + classes.append(class_name) + + # Implementation declarations + elif line.startswith('@implementation '): + impl_match = re.match(r'@implementation\s+(\w+)', line) + if impl_match: + current_class = impl_match.group(1) + + # Method declarations + elif line.startswith(('- (', '+ (')): + method_match = re.search(r'[+-]\s*\([^)]+\)\s*(\w+)', line) + if method_match: + method_name = method_match.group(1) + full_name = f"{current_class}.{method_name}" if current_class else method_name + symbol_id = self._create_symbol_id(file_path, full_name) + symbols[symbol_id] = SymbolInfo( + type="method", + file=file_path, + line=i + 1, + signature=line + ) + functions.append(full_name) + + # C function declarations + elif re.match(r'\w+.*\s+\w+\s*\([^)]*\)\s*\{?', line) and not line.startswith(('if', 'for', 'while')): + func_match = re.search(r'\s(\w+)\s*\([^)]*\)', line) + if func_match: + func_name = func_match.group(1) + symbol_id = self._create_symbol_id(file_path, func_name) + symbols[symbol_id] = SymbolInfo( + type="function", + file=file_path, + line=i + 1, + signature=line + ) + functions.append(func_name) + + # End of class + elif line == '@end': + current_class = None + + # Phase 2: Add call relationship analysis + self._analyze_objc_calls(content, symbols, file_path) + + file_info = FileInfo( + language=self.get_language_name(), + line_count=len(lines), + symbols={"functions": functions, "classes": classes}, + imports=imports + ) + + return symbols, file_info + + def _analyze_objc_calls(self, content: str, symbols: Dict[str, SymbolInfo], file_path: str): + """Analyze Objective-C method calls for relationships.""" + lines = content.splitlines() + current_function = None + + for i, line in enumerate(lines): + original_line = line + line = line.strip() + + # Track current method context + if line.startswith('- (') or line.startswith('+ ('): + func_name = self._extract_objc_method_name(line) + if func_name: + current_function = self._create_symbol_id(file_path, func_name) + + # Find method calls: [obj methodName] or functionName() + if current_function and ('[' in line and ']' in line or ('(' in line and ')' in line)): + called_functions = self._extract_objc_called_functions(line) + for called_func in called_functions: + # Find the called function in symbols and add relationship + for symbol_id, symbol_info in symbols.items(): + if called_func in symbol_id.split("::")[-1]: + if current_function not in symbol_info.called_by: + symbol_info.called_by.append(current_function) + + def _extract_objc_method_name(self, line: str) -> Optional[str]: + """Extract method name from Objective-C method declaration.""" + try: + # - (returnType)methodName:(params) or + (returnType)methodName + match = re.search(r'[+-]\s*\([^)]*\)\s*(\w+)', line) + if match: + return match.group(1) + except: + pass + return None + + def _extract_objc_called_functions(self, line: str) -> List[str]: + """Extract method names that are being called in this line.""" + called_functions = [] + + # Find patterns like: [obj methodName] or functionName( + patterns = [ + r'\[\s*\w+\s+(\w+)\s*[\]:]', # [obj methodName] + r'(\w+)\s*\(', # functionName( + ] + + for pattern in patterns: + matches = re.findall(pattern, line) + called_functions.extend(matches) + + return called_functions diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/python_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/python_strategy.py new file mode 100644 index 00000000..ee1dae95 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/python_strategy.py @@ -0,0 +1,367 @@ +""" +Python parsing strategy using AST - Optimized single-pass version. +""" + +import ast +import logging +from typing import Dict, List, Tuple, Optional, Set +from .base_strategy import ParsingStrategy +from ..models import SymbolInfo, FileInfo + +logger = logging.getLogger(__name__) + + +class PythonParsingStrategy(ParsingStrategy): + """Python-specific parsing strategy using Python's built-in AST - Single Pass Optimized.""" + + def get_language_name(self) -> str: + return "python" + + def get_supported_extensions(self) -> List[str]: + return ['.py', '.pyw'] + + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Parse Python file using AST with single-pass optimization.""" + symbols = {} + functions = [] + classes = [] + imports = [] + + try: + tree = ast.parse(content) + # Single-pass visitor that handles everything at once + visitor = SinglePassVisitor(symbols, functions, classes, imports, file_path) + visitor.visit(tree) + except SyntaxError as e: + logger.warning(f"Syntax error in Python file {file_path}: {e}") + except Exception as e: + logger.warning(f"Error parsing Python file {file_path}: {e}") + + file_info = FileInfo( + language=self.get_language_name(), + line_count=len(content.splitlines()), + symbols={"functions": functions, "classes": classes}, + imports=imports + ) + + pending_calls = visitor.resolve_deferred_calls() + if pending_calls: + file_info.pending_calls = pending_calls + + return symbols, file_info + + +class SinglePassVisitor(ast.NodeVisitor): + """Single-pass AST visitor that extracts symbols and analyzes calls in one traversal.""" + + def __init__(self, symbols: Dict[str, SymbolInfo], functions: List[str], + classes: List[str], imports: List[str], file_path: str): + self.symbols = symbols + self.functions = functions + self.classes = classes + self.imports = imports + self.file_path = file_path + + # Context tracking for call analysis + self.current_function_stack = [] + self.current_class = None + self.variable_type_stack: List[Dict[str, str]] = [{}] + + # Symbol lookup index for O(1) access + self.symbol_lookup = {} # name -> symbol_id mapping for fast lookups + + # Track processed nodes to avoid duplicates + self.processed_nodes: Set[int] = set() + # Deferred call relationships for forward references + self.deferred_calls: List[Tuple[str, str]] = [] + + def visit_ClassDef(self, node: ast.ClassDef): + """Visit class definition - extract symbol and analyze in single pass.""" + class_name = node.name + symbol_id = self._create_symbol_id(self.file_path, class_name) + + # Extract docstring + docstring = ast.get_docstring(node) + + # Create symbol info + symbol_info = SymbolInfo( + type="class", + file=self.file_path, + line=node.lineno, + docstring=docstring + ) + + # Store in symbols and lookup index + self.symbols[symbol_id] = symbol_info + self.symbol_lookup[class_name] = symbol_id + self.classes.append(class_name) + + # Track class context for method processing + old_class = self.current_class + self.current_class = class_name + + method_nodes = [] + # First pass: register methods so forward references resolve + for child in node.body: + if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)): + self._register_method(child, class_name) + method_nodes.append(child) + else: + self.visit(child) + + # Second pass: visit method bodies for call analysis + for method_node in method_nodes: + self._visit_registered_method(method_node, class_name) + + # Restore previous class context + self.current_class = old_class + + def visit_FunctionDef(self, node: ast.FunctionDef): + """Visit function definition - extract symbol and track context.""" + self._process_function(node) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): + """Visit async function definition - extract symbol and track context.""" + self._process_function(node) + + def _process_function(self, node): + """Process both sync and async function definitions.""" + # Skip if this is a method (already handled by ClassDef) + if self.current_class: + return + + # Skip if already processed + node_id = id(node) + if node_id in self.processed_nodes: + return + self.processed_nodes.add(node_id) + + func_name = node.name + symbol_id = self._create_symbol_id(self.file_path, func_name) + + # Extract function signature and docstring + signature = self._extract_function_signature(node) + docstring = ast.get_docstring(node) + + # Create symbol info + symbol_info = SymbolInfo( + type="function", + file=self.file_path, + line=node.lineno, + signature=signature, + docstring=docstring + ) + + # Store in symbols and lookup index + self.symbols[symbol_id] = symbol_info + self.symbol_lookup[func_name] = symbol_id + self.functions.append(func_name) + + # Track function context for call analysis + function_id = f"{self.file_path}::{func_name}" + self.variable_type_stack.append({}) + self.current_function_stack.append(function_id) + + # Visit function body to analyze calls + self.generic_visit(node) + + # Pop function from stack + self.current_function_stack.pop() + self.variable_type_stack.pop() + + def visit_Assign(self, node: ast.Assign): + """Track simple variable assignments to class instances.""" + class_name = self._infer_class_name(node.value) + if class_name: + current_scope = self._current_var_types() + for target in node.targets: + if isinstance(target, ast.Name): + current_scope[target.id] = class_name + self.generic_visit(node) + + def visit_AnnAssign(self, node: ast.AnnAssign): + """Track annotated assignments that instantiate classes.""" + class_name = self._infer_class_name(node.value) + if class_name and isinstance(node.target, ast.Name): + self._current_var_types()[node.target.id] = class_name + self.generic_visit(node) + + def _current_var_types(self) -> Dict[str, str]: + return self.variable_type_stack[-1] + + def _infer_class_name(self, value: Optional[ast.AST]) -> Optional[str]: + if isinstance(value, ast.Call): + func = value.func + if isinstance(func, ast.Name): + return func.id + if isinstance(func, ast.Attribute): + return func.attr + return None + + def _register_method(self, node: ast.FunctionDef, class_name: str): + """Register a method symbol without visiting its body.""" + method_name = f"{class_name}.{node.name}" + method_symbol_id = self._create_symbol_id(self.file_path, method_name) + + method_signature = self._extract_function_signature(node) + method_docstring = ast.get_docstring(node) + + symbol_info = SymbolInfo( + type="method", + file=self.file_path, + line=node.lineno, + signature=method_signature, + docstring=method_docstring + ) + + self.symbols[method_symbol_id] = symbol_info + self.symbol_lookup[method_name] = method_symbol_id + self.symbol_lookup[node.name] = method_symbol_id # Also index by short method name + self.functions.append(method_name) + + def _visit_registered_method(self, node: ast.FunctionDef, class_name: str): + """Visit a previously registered method body for call analysis.""" + method_name = f"{class_name}.{node.name}" + function_id = f"{self.file_path}::{method_name}" + self.variable_type_stack.append({}) + self.current_function_stack.append(function_id) + for child in node.body: + self.visit(child) + self.current_function_stack.pop() + self.variable_type_stack.pop() + + def visit_Import(self, node: ast.Import): + """Handle import statements.""" + for alias in node.names: + self.imports.append(alias.name) + self.generic_visit(node) + + def visit_ImportFrom(self, node: ast.ImportFrom): + """Handle from...import statements.""" + if node.module: + for alias in node.names: + self.imports.append(f"{node.module}.{alias.name}") + self.generic_visit(node) + + def visit_Call(self, node: ast.Call): + """Visit function call and record relationship using O(1) lookup.""" + if not self.current_function_stack: + self.generic_visit(node) + return + + try: + # Get the function name being called + called_function = None + + if isinstance(node.func, ast.Name): + # Direct function call: function_name() + called_function = self._qualify_name(node.func.id) + elif isinstance(node.func, ast.Attribute): + # Method call: obj.method() or module.function() + if not self._is_super_call(node.func): + qualifier = self._infer_attribute_qualifier(node.func.value) + if qualifier: + called_function = f"{qualifier}.{node.func.attr}" + else: + called_function = node.func.attr + + if called_function: + caller_function = self.current_function_stack[-1] + if not self._register_call_relationship(caller_function, called_function): + self.deferred_calls.append((caller_function, called_function)) + except Exception: + # Silently handle parsing errors for complex call patterns + pass + + # Continue visiting child nodes + self.generic_visit(node) + + def _register_call_relationship(self, caller_function: str, called_function: str) -> bool: + """Attempt to resolve a call relationship immediately.""" + try: + if called_function in self.symbol_lookup: + symbol_id = self.symbol_lookup[called_function] + symbol_info = self.symbols[symbol_id] + if symbol_info.type in ["function", "method"]: + if caller_function not in symbol_info.called_by: + symbol_info.called_by.append(caller_function) + return True + + for name, symbol_id in self.symbol_lookup.items(): + if name.endswith(f".{called_function}"): + symbol_info = self.symbols[symbol_id] + if symbol_info.type in ["function", "method"]: + if caller_function not in symbol_info.called_by: + symbol_info.called_by.append(caller_function) + return True + except Exception: + return False + + return False + + def _qualify_name(self, name: str) -> str: + """Map bare identifiers to fully qualified symbol names.""" + if name in self.symbol_lookup: + return name + if name and name[0].isupper(): + return f"{name}.__init__" + return name + + def _infer_attribute_qualifier(self, value: ast.AST) -> Optional[str]: + """Infer class name for attribute-based calls.""" + if isinstance(value, ast.Name): + return self._current_var_types().get(value.id) + if isinstance(value, ast.Call): + return self._infer_class_name(value) + if isinstance(value, ast.Attribute): + if isinstance(value.value, ast.Name): + inferred = self._current_var_types().get(value.value.id) + if inferred: + return inferred + return value.attr + return None + + def resolve_deferred_calls(self) -> List[Tuple[str, str]]: + """Resolve stored call relationships once all symbols are known.""" + if not self.deferred_calls: + return [] + current = list(self.deferred_calls) + unresolved: List[Tuple[str, str]] = [] + self.deferred_calls.clear() + for caller, called in current: + if not self._register_call_relationship(caller, called): + unresolved.append((caller, called)) + self.deferred_calls = unresolved + return unresolved + + @staticmethod + def _is_super_call(attr_node: ast.Attribute) -> bool: + """Detect super().method(...) patterns.""" + value = attr_node.value + if isinstance(value, ast.Call) and isinstance(value.func, ast.Name): + return value.func.id == "super" + return False + + def _create_symbol_id(self, file_path: str, symbol_name: str) -> str: + """Create a unique symbol ID.""" + return f"{file_path}::{symbol_name}" + + def _extract_function_signature(self, node: ast.FunctionDef) -> str: + """Extract function signature from AST node.""" + # Build basic signature + args = [] + + # Regular arguments + for arg in node.args.args: + args.append(arg.arg) + + # Varargs (*args) + if node.args.vararg: + args.append(f"*{node.args.vararg.arg}") + + # Keyword arguments (**kwargs) + if node.args.kwarg: + args.append(f"**{node.args.kwarg.arg}") + + signature = f"def {node.name}({', '.join(args)}):" + return signature diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/strategy_factory.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/strategy_factory.py new file mode 100644 index 00000000..c7116d96 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/strategy_factory.py @@ -0,0 +1,201 @@ +""" +Strategy factory for creating appropriate parsing strategies. +""" + +import threading +from typing import Dict, List +from .base_strategy import ParsingStrategy +from .python_strategy import PythonParsingStrategy +from .javascript_strategy import JavaScriptParsingStrategy +from .typescript_strategy import TypeScriptParsingStrategy +from .java_strategy import JavaParsingStrategy +from .go_strategy import GoParsingStrategy +from .objective_c_strategy import ObjectiveCParsingStrategy +from .zig_strategy import ZigParsingStrategy +from .fallback_strategy import FallbackParsingStrategy + + +class StrategyFactory: + """Factory for creating appropriate parsing strategies.""" + + def __init__(self): + # Initialize all strategies with thread safety + self._strategies: Dict[str, ParsingStrategy] = {} + self._initialized = False + self._lock = threading.RLock() + self._initialize_strategies() + + # File type mappings for fallback parser + self._file_type_mappings = { + # Web and markup + '.html': 'html', '.htm': 'html', + '.css': 'css', '.scss': 'css', '.sass': 'css', + '.less': 'css', '.stylus': 'css', '.styl': 'css', + '.md': 'markdown', '.mdx': 'markdown', + '.json': 'json', '.jsonc': 'json', + '.xml': 'xml', + '.yml': 'yaml', '.yaml': 'yaml', + + # Frontend frameworks + '.vue': 'vue', + '.svelte': 'svelte', + '.astro': 'astro', + + # Template engines + '.hbs': 'handlebars', '.handlebars': 'handlebars', + '.ejs': 'ejs', + '.pug': 'pug', + + # Database and SQL + '.sql': 'sql', '.ddl': 'sql', '.dml': 'sql', + '.mysql': 'sql', '.postgresql': 'sql', '.psql': 'sql', + '.sqlite': 'sql', '.mssql': 'sql', '.oracle': 'sql', + '.ora': 'sql', '.db2': 'sql', + '.proc': 'sql', '.procedure': 'sql', + '.func': 'sql', '.function': 'sql', + '.view': 'sql', '.trigger': 'sql', '.index': 'sql', + '.migration': 'sql', '.seed': 'sql', '.fixture': 'sql', + '.schema': 'sql', + '.cql': 'sql', '.cypher': 'sql', '.sparql': 'sql', + '.gql': 'graphql', + '.liquibase': 'sql', '.flyway': 'sql', + + # Config and text files + '.txt': 'text', + '.ini': 'config', '.cfg': 'config', '.conf': 'config', + '.toml': 'config', + '.properties': 'config', + '.env': 'config', + '.gitignore': 'config', + '.dockerignore': 'config', + '.editorconfig': 'config', + + # Other programming languages (will use fallback) + '.c': 'c', '.cpp': 'cpp', '.h': 'h', '.hpp': 'hpp', + '.cxx': 'cpp', '.cc': 'cpp', '.hxx': 'hpp', '.hh': 'hpp', + '.cs': 'csharp', + '.rb': 'ruby', + '.php': 'php', + '.swift': 'swift', + '.kt': 'kotlin', '.kts': 'kotlin', + '.rs': 'rust', + '.scala': 'scala', + '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', + '.ps1': 'powershell', + '.bat': 'batch', '.cmd': 'batch', + '.r': 'r', '.R': 'r', + '.pl': 'perl', '.pm': 'perl', + '.lua': 'lua', + '.dart': 'dart', + '.hs': 'haskell', + '.ml': 'ocaml', '.mli': 'ocaml', + '.fs': 'fsharp', '.fsx': 'fsharp', + '.clj': 'clojure', '.cljs': 'clojure', + '.vim': 'vim', + } + + def _initialize_strategies(self): + """Initialize all parsing strategies with thread safety.""" + with self._lock: + if self._initialized: + return + + try: + # Python + python_strategy = PythonParsingStrategy() + for ext in python_strategy.get_supported_extensions(): + self._strategies[ext] = python_strategy + + # JavaScript + js_strategy = JavaScriptParsingStrategy() + for ext in js_strategy.get_supported_extensions(): + self._strategies[ext] = js_strategy + + # TypeScript + ts_strategy = TypeScriptParsingStrategy() + for ext in ts_strategy.get_supported_extensions(): + self._strategies[ext] = ts_strategy + + # Java + java_strategy = JavaParsingStrategy() + for ext in java_strategy.get_supported_extensions(): + self._strategies[ext] = java_strategy + + # Go + go_strategy = GoParsingStrategy() + for ext in go_strategy.get_supported_extensions(): + self._strategies[ext] = go_strategy + + # Objective-C + objc_strategy = ObjectiveCParsingStrategy() + for ext in objc_strategy.get_supported_extensions(): + self._strategies[ext] = objc_strategy + + # Zig + zig_strategy = ZigParsingStrategy() + for ext in zig_strategy.get_supported_extensions(): + self._strategies[ext] = zig_strategy + + self._initialized = True + + except Exception as e: + # Reset state on failure to allow retry + self._strategies.clear() + self._initialized = False + raise e + + def get_strategy(self, file_extension: str) -> ParsingStrategy: + """ + Get appropriate strategy for file extension. + + Args: + file_extension: File extension (e.g., '.py', '.js') + + Returns: + Appropriate parsing strategy + """ + with self._lock: + # Ensure initialization is complete + if not self._initialized: + self._initialize_strategies() + + # Check for specialized strategies first + if file_extension in self._strategies: + return self._strategies[file_extension] + + # Use fallback strategy with appropriate language name + language_name = self._file_type_mappings.get(file_extension, 'unknown') + return FallbackParsingStrategy(language_name) + + def get_all_supported_extensions(self) -> List[str]: + """Get all supported extensions across strategies.""" + specialized = list(self._strategies.keys()) + fallback = list(self._file_type_mappings.keys()) + return specialized + fallback + + def get_specialized_extensions(self) -> List[str]: + """Get extensions that have specialized parsers.""" + return list(self._strategies.keys()) + + def get_fallback_extensions(self) -> List[str]: + """Get extensions that use fallback parsing.""" + return list(self._file_type_mappings.keys()) + + def get_strategy_info(self) -> Dict[str, List[str]]: + """Get information about available strategies.""" + info = {} + + # Group extensions by strategy type + for ext, strategy in self._strategies.items(): + strategy_name = strategy.get_language_name() + if strategy_name not in info: + info[strategy_name] = [] + info[strategy_name].append(ext) + + # Add fallback info + fallback_languages = set(self._file_type_mappings.values()) + for lang in fallback_languages: + extensions = [ext for ext, mapped_lang in self._file_type_mappings.items() if mapped_lang == lang] + info[f"fallback_{lang}"] = extensions + + return info diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/typescript_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/typescript_strategy.py new file mode 100644 index 00000000..002d2b83 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/typescript_strategy.py @@ -0,0 +1,487 @@ +""" +TypeScript parsing strategy using tree-sitter - Optimized single-pass version. +""" + +import logging +from typing import Dict, List, Tuple, Optional, Set +from .base_strategy import ParsingStrategy +from ..models import SymbolInfo, FileInfo + +logger = logging.getLogger(__name__) + +import tree_sitter +from tree_sitter_typescript import language_typescript + + +class TypeScriptParsingStrategy(ParsingStrategy): + """TypeScript-specific parsing strategy using tree-sitter - Single Pass Optimized.""" + + def __init__(self): + self.ts_language = tree_sitter.Language(language_typescript()) + + def get_language_name(self) -> str: + return "typescript" + + def get_supported_extensions(self) -> List[str]: + return ['.ts', '.tsx'] + + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Parse TypeScript file using tree-sitter with single-pass optimization.""" + symbols = {} + functions = [] + classes = [] + imports = [] + exports = [] + + # Symbol lookup index for O(1) access + symbol_lookup = {} # name -> symbol_id mapping + pending_calls: List[Tuple[str, str]] = [] + pending_call_set: Set[Tuple[str, str]] = set() + variable_scopes: List[Dict[str, str]] = [{}] + + parser = tree_sitter.Parser(self.ts_language) + tree = parser.parse(content.encode('utf8')) + + # Single-pass traversal that handles everything + context = TraversalContext( + content=content, + file_path=file_path, + symbols=symbols, + functions=functions, + classes=classes, + imports=imports, + exports=exports, + symbol_lookup=symbol_lookup, + pending_calls=pending_calls, + pending_call_set=pending_call_set, + variable_scopes=variable_scopes, + ) + + self._traverse_node_single_pass(tree.root_node, context) + + file_info = FileInfo( + language=self.get_language_name(), + line_count=len(content.splitlines()), + symbols={"functions": functions, "classes": classes}, + imports=imports, + exports=exports + ) + + if context.pending_calls: + file_info.pending_calls = context.pending_calls + + return symbols, file_info + + def _traverse_node_single_pass(self, node, context: 'TraversalContext', + current_function: Optional[str] = None, + current_class: Optional[str] = None): + """Single-pass traversal that extracts symbols and analyzes calls.""" + + node_type = node.type + + # Handle function declarations + if node_type == 'function_declaration': + name = self._get_function_name(node, context.content) + if name: + symbol_id = self._create_symbol_id(context.file_path, name) + signature = self._get_ts_function_signature(node, context.content) + symbol_info = SymbolInfo( + type="function", + file=context.file_path, + line=node.start_point[0] + 1, + signature=signature + ) + context.symbols[symbol_id] = symbol_info + context.symbol_lookup[name] = symbol_id + context.functions.append(name) + + # Traverse function body with updated context + func_context = f"{context.file_path}::{name}" + for child in node.children: + self._traverse_node_single_pass(child, context, current_function=func_context, + current_class=current_class) + return + + # Handle class declarations + elif node_type == 'class_declaration': + name = self._get_class_name(node, context.content) + if name: + symbol_id = self._create_symbol_id(context.file_path, name) + symbol_info = SymbolInfo( + type="class", + file=context.file_path, + line=node.start_point[0] + 1 + ) + context.symbols[symbol_id] = symbol_info + context.symbol_lookup[name] = symbol_id + context.classes.append(name) + + # Traverse class body with updated context + for child in node.children: + self._traverse_node_single_pass(child, context, current_function=current_function, + current_class=name) + return + + # Handle interface declarations + elif node_type == 'interface_declaration': + name = self._get_interface_name(node, context.content) + if name: + symbol_id = self._create_symbol_id(context.file_path, name) + symbol_info = SymbolInfo( + type="interface", + file=context.file_path, + line=node.start_point[0] + 1 + ) + context.symbols[symbol_id] = symbol_info + context.symbol_lookup[name] = symbol_id + context.classes.append(name) # Group interfaces with classes + + # Traverse interface body with updated context + for child in node.children: + self._traverse_node_single_pass(child, context, current_function=current_function, + current_class=name) + return + + # Handle method definitions + elif node_type == 'method_definition': + method_name = self._get_method_name(node, context.content) + if method_name and current_class: + full_name = f"{current_class}.{method_name}" + symbol_id = self._create_symbol_id(context.file_path, full_name) + signature = self._get_ts_function_signature(node, context.content) + symbol_info = SymbolInfo( + type="method", + file=context.file_path, + line=node.start_point[0] + 1, + signature=signature + ) + context.symbols[symbol_id] = symbol_info + context.symbol_lookup[full_name] = symbol_id + context.symbol_lookup[method_name] = symbol_id # Also index by method name alone + context.functions.append(full_name) + + # Traverse method body with updated context + method_context = f"{context.file_path}::{full_name}" + for child in node.children: + self._traverse_node_single_pass(child, context, current_function=method_context, + current_class=current_class) + return + + # Handle variable declarations that define callable exports + elif node_type in ['lexical_declaration', 'variable_statement']: + handled = False + for child in node.children: + if child.type != 'variable_declarator': + continue + name_node = child.child_by_field_name('name') + value_node = child.child_by_field_name('value') + if not name_node or not value_node: + continue + + if current_function is not None: + continue + + value_type = value_node.type + if value_type not in [ + 'arrow_function', + 'function', + 'function_expression', + 'call_expression', + 'new_expression', + 'identifier', + 'member_expression', + ]: + continue + + name = context.content[name_node.start_byte:name_node.end_byte] + symbol_id = self._create_symbol_id(context.file_path, name) + signature = context.content[child.start_byte:child.end_byte].split('\n')[0].strip() + symbol_info = SymbolInfo( + type="function", + file=context.file_path, + line=child.start_point[0] + 1, + signature=signature + ) + context.symbols[symbol_id] = symbol_info + context.symbol_lookup[name] = symbol_id + context.functions.append(name) + handled = True + + if value_type in ['arrow_function', 'function', 'function_expression']: + func_context = f"{context.file_path}::{name}" + context.variable_scopes.append({}) + self._traverse_node_single_pass( + value_node, + context, + current_function=func_context, + current_class=current_class + ) + context.variable_scopes.pop() + + if handled: + return + + # Handle function calls + elif node_type == 'call_expression': + caller = current_function or f"{context.file_path}:{node.start_point[0] + 1}" + called_function = self._resolve_called_function(node, context, current_class) + if caller and called_function: + self._register_call(context, caller, called_function) + if caller: + self._collect_callback_arguments(node, context, caller, current_class, current_function) + + # Handle import declarations + elif node.type == 'import_statement': + import_text = context.content[node.start_byte:node.end_byte] + context.imports.append(import_text) + + # Handle export declarations + elif node.type in ['export_statement', 'export_default_declaration']: + export_text = context.content[node.start_byte:node.end_byte] + context.exports.append(export_text) + + # Continue traversing children for other node types + for child in node.children: + self._traverse_node_single_pass(child, context, current_function=current_function, + current_class=current_class) + + def _register_call(self, context: 'TraversalContext', caller: str, called: str) -> None: + if called in context.symbol_lookup: + symbol_id = context.symbol_lookup[called] + symbol_info = context.symbols[symbol_id] + if caller not in symbol_info.called_by: + symbol_info.called_by.append(caller) + return + + key = (caller, called) + if key not in context.pending_call_set: + context.pending_call_set.add(key) + context.pending_calls.append(key) + + def _collect_callback_arguments( + self, + node, + context: 'TraversalContext', + caller: str, + current_class: Optional[str], + current_function: Optional[str] + ) -> None: + arguments_node = node.child_by_field_name('arguments') + if not arguments_node: + return + + for argument in arguments_node.children: + if not getattr(argument, "is_named", False): + continue + callback_name = self._resolve_argument_reference(argument, context, current_class) + if callback_name: + call_site = caller + if current_function is None: + call_site = f"{context.file_path}:{argument.start_point[0] + 1}" + self._register_call(context, call_site, callback_name) + + def _resolve_argument_reference( + self, + node, + context: 'TraversalContext', + current_class: Optional[str] + ) -> Optional[str]: + node_type = node.type + + if node_type == 'identifier': + return context.content[node.start_byte:node.end_byte] + + if node_type == 'member_expression': + property_node = node.child_by_field_name('property') + if property_node is None: + for child in node.children: + if child.type in ['property_identifier', 'identifier']: + property_node = child + break + if property_node is None: + return None + + property_name = context.content[property_node.start_byte:property_node.end_byte] + qualifier_node = node.child_by_field_name('object') + qualifier = self._resolve_member_qualifier( + qualifier_node, + context, + current_class + ) + if not qualifier: + for child in node.children: + if child is property_node: + continue + qualifier = self._resolve_member_qualifier( + child, + context, + current_class + ) + if qualifier: + break + if qualifier: + return f"{qualifier}.{property_name}" + return property_name + + return None + + def _resolve_called_function( + self, + node, + context: 'TraversalContext', + current_class: Optional[str] + ) -> Optional[str]: + function_node = node.child_by_field_name('function') + if function_node is None and node.children: + function_node = node.children[0] + if function_node is None: + return None + + if function_node.type == 'identifier': + return context.content[function_node.start_byte:function_node.end_byte] + + if function_node.type == 'member_expression': + property_node = function_node.child_by_field_name('property') + if property_node is None: + for child in function_node.children: + if child.type in ['property_identifier', 'identifier']: + property_node = child + break + if property_node is None: + return None + + property_name = context.content[property_node.start_byte:property_node.end_byte] + qualifier_node = function_node.child_by_field_name('object') + qualifier = self._resolve_member_qualifier( + qualifier_node, + context, + current_class + ) + if not qualifier: + for child in function_node.children: + if child is property_node: + continue + qualifier = self._resolve_member_qualifier( + child, + context, + current_class + ) + if qualifier: + break + if qualifier: + return f"{qualifier}.{property_name}" + return property_name + + return None + + def _resolve_member_qualifier( + self, + node, + context: 'TraversalContext', + current_class: Optional[str] + ) -> Optional[str]: + if node is None: + return None + + node_type = node.type + if node_type == 'this': + return current_class + + if node_type == 'identifier': + return context.content[node.start_byte:node.end_byte] + + if node_type == 'member_expression': + property_node = node.child_by_field_name('property') + if property_node is None: + for child in node.children: + if child.type in ['property_identifier', 'identifier']: + property_node = child + break + if property_node is None: + return None + + qualifier = self._resolve_member_qualifier( + node.child_by_field_name('object'), + context, + current_class + ) + if not qualifier: + for child in node.children: + if child is property_node: + continue + qualifier = self._resolve_member_qualifier( + child, + context, + current_class + ) + if qualifier: + break + + property_name = context.content[property_node.start_byte:property_node.end_byte] + if qualifier: + return f"{qualifier}.{property_name}" + return property_name + + return None + + def _get_function_name(self, node, content: str) -> Optional[str]: + """Extract function name from tree-sitter node.""" + for child in node.children: + if child.type == 'identifier': + return content[child.start_byte:child.end_byte] + return None + + def _get_class_name(self, node, content: str) -> Optional[str]: + """Extract class name from tree-sitter node.""" + for child in node.children: + if child.type == 'identifier': + return content[child.start_byte:child.end_byte] + return None + + def _get_interface_name(self, node, content: str) -> Optional[str]: + """Extract interface name from tree-sitter node.""" + for child in node.children: + if child.type == 'type_identifier': + return content[child.start_byte:child.end_byte] + return None + + def _get_method_name(self, node, content: str) -> Optional[str]: + """Extract method name from tree-sitter node.""" + for child in node.children: + if child.type == 'property_identifier': + return content[child.start_byte:child.end_byte] + return None + + def _get_ts_function_signature(self, node, content: str) -> str: + """Extract TypeScript function signature.""" + return content[node.start_byte:node.end_byte].split('\n')[0].strip() + + +class TraversalContext: + """Context object to pass state during single-pass traversal.""" + + def __init__( + self, + content: str, + file_path: str, + symbols: Dict, + functions: List, + classes: List, + imports: List, + exports: List, + symbol_lookup: Dict, + pending_calls: List[Tuple[str, str]], + pending_call_set: Set[Tuple[str, str]], + variable_scopes: List[Dict[str, str]], + ): + self.content = content + self.file_path = file_path + self.symbols = symbols + self.functions = functions + self.classes = classes + self.imports = imports + self.exports = exports + self.symbol_lookup = symbol_lookup + self.pending_calls = pending_calls + self.pending_call_set = pending_call_set + self.variable_scopes = variable_scopes diff --git a/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/zig_strategy.py b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/zig_strategy.py new file mode 100644 index 00000000..658ca2bc --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/indexing/strategies/zig_strategy.py @@ -0,0 +1,99 @@ +""" +Zig parsing strategy using tree-sitter. +""" + +import logging +from typing import Dict, List, Tuple, Optional +from .base_strategy import ParsingStrategy +from ..models import SymbolInfo, FileInfo + +logger = logging.getLogger(__name__) + +import tree_sitter +from tree_sitter_zig import language + + +class ZigParsingStrategy(ParsingStrategy): + """Zig parsing strategy using tree-sitter.""" + + def __init__(self): + self.zig_language = tree_sitter.Language(language()) + + def get_language_name(self) -> str: + return "zig" + + def get_supported_extensions(self) -> List[str]: + return ['.zig', '.zon'] + + def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Parse Zig file using tree-sitter.""" + return self._tree_sitter_parse(file_path, content) + + + def _tree_sitter_parse(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo], FileInfo]: + """Parse Zig file using tree-sitter.""" + symbols = {} + functions = [] + classes = [] + imports = [] + + parser = tree_sitter.Parser(self.zig_language) + tree = parser.parse(content.encode('utf8')) + + # Phase 1: Extract symbols using tree-sitter + self._traverse_zig_node(tree.root_node, content, file_path, symbols, functions, classes, imports) + + file_info = FileInfo( + language=self.get_language_name(), + line_count=len(content.splitlines()), + symbols={"functions": functions, "classes": classes}, + imports=imports + ) + + return symbols, file_info + + def _traverse_zig_node(self, node, content: str, file_path: str, symbols: Dict, functions: List, classes: List, imports: List): + """Traverse Zig AST node and extract symbols.""" + if node.type == 'function_declaration': + func_name = self._extract_zig_function_name_from_node(node, content) + if func_name: + line_number = self._extract_line_number(content, node.start_byte) + symbol_id = self._create_symbol_id(file_path, func_name) + symbols[symbol_id] = SymbolInfo( + type="function", + file=file_path, + line=line_number, + signature=self._safe_extract_text(content, node.start_byte, node.end_byte) + ) + functions.append(func_name) + + elif node.type in ['struct_declaration', 'union_declaration', 'enum_declaration']: + type_name = self._extract_zig_type_name_from_node(node, content) + if type_name: + line_number = self._extract_line_number(content, node.start_byte) + symbol_id = self._create_symbol_id(file_path, type_name) + symbols[symbol_id] = SymbolInfo( + type=node.type.replace('_declaration', ''), + file=file_path, + line=line_number + ) + classes.append(type_name) + + # Recurse through children + for child in node.children: + self._traverse_zig_node(child, content, file_path, symbols, functions, classes, imports) + + def _extract_zig_function_name_from_node(self, node, content: str) -> Optional[str]: + """Extract function name from tree-sitter node.""" + for child in node.children: + if child.type == 'identifier': + return self._safe_extract_text(content, child.start_byte, child.end_byte) + return None + + def _extract_zig_type_name_from_node(self, node, content: str) -> Optional[str]: + """Extract type name from tree-sitter node.""" + for child in node.children: + if child.type == 'identifier': + return self._safe_extract_text(content, child.start_byte, child.end_byte) + return None + diff --git a/reference/code-index-mcp-master/src/code_index_mcp/project_settings.py b/reference/code-index-mcp-master/src/code_index_mcp/project_settings.py new file mode 100644 index 00000000..d3c39651 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/project_settings.py @@ -0,0 +1,514 @@ +""" +Project Settings Management + +This module provides functionality for managing project settings and persistent data +for the Code Index MCP server. +""" +import os +import json + + +import tempfile +import hashlib + +from datetime import datetime + + +from .constants import ( + SETTINGS_DIR, CONFIG_FILE, INDEX_FILE +) +from .search.base import SearchStrategy +from .search.ugrep import UgrepStrategy +from .search.ripgrep import RipgrepStrategy +from .search.ag import AgStrategy +from .search.grep import GrepStrategy +from .search.basic import BasicSearchStrategy + + +# Prioritized list of search strategies +SEARCH_STRATEGY_CLASSES = [ + UgrepStrategy, + RipgrepStrategy, + AgStrategy, + GrepStrategy, + BasicSearchStrategy, +] + + +def _get_available_strategies() -> list[SearchStrategy]: + """ + Detect and return a list of available search strategy instances, + ordered by preference. + """ + available = [] + for strategy_class in SEARCH_STRATEGY_CLASSES: + try: + strategy = strategy_class() + if strategy.is_available(): + available.append(strategy) + except Exception: + pass + return available + + +class ProjectSettings: + """Class for managing project settings and index data""" + + def __init__(self, base_path, skip_load=False): + """Initialize project settings + + Args: + base_path (str): Base path of the project + skip_load (bool): Whether to skip loading files + """ + self.base_path = base_path + self.skip_load = skip_load + self.available_strategies: list[SearchStrategy] = [] + self.refresh_available_strategies() + + # Ensure the base path of the temporary directory exists + try: + # Get system temporary directory + system_temp = tempfile.gettempdir() + + # Check if the system temporary directory exists and is writable + if not os.path.exists(system_temp): + # Try using project directory as fallback if available + if base_path and os.path.exists(base_path): + system_temp = base_path + else: + # Use user's home directory as last resort + system_temp = os.path.expanduser("~") + + if not os.access(system_temp, os.W_OK): + # Try using project directory as fallback if available + if base_path and os.path.exists(base_path) and os.access(base_path, os.W_OK): + system_temp = base_path + else: + # Use user's home directory as last resort + system_temp = os.path.expanduser("~") + + # Create code_indexer directory + temp_base_dir = os.path.join(system_temp, SETTINGS_DIR) + + if not os.path.exists(temp_base_dir): + os.makedirs(temp_base_dir, exist_ok=True) + else: + pass + except Exception: + # If unable to create temporary directory, use .code_indexer in project directory if available + if base_path and os.path.exists(base_path): + temp_base_dir = os.path.join(base_path, ".code_indexer") + + else: + # Use home directory as last resort + temp_base_dir = os.path.join(os.path.expanduser("~"), ".code_indexer") + + if not os.path.exists(temp_base_dir): + os.makedirs(temp_base_dir, exist_ok=True) + + # Use system temporary directory to store index data + try: + if base_path: + # Use hash of project path as unique identifier + path_hash = hashlib.md5(base_path.encode()).hexdigest() + self.settings_path = os.path.join(temp_base_dir, path_hash) + else: + # If no base path provided, use a default directory + self.settings_path = os.path.join(temp_base_dir, "default") + + self.ensure_settings_dir() + except Exception: + # If error occurs, use .code_indexer in project or home directory as fallback + if base_path and os.path.exists(base_path): + fallback_dir = os.path.join(base_path, ".code_indexer", + hashlib.md5(base_path.encode()).hexdigest()) + else: + fallback_dir = os.path.join(os.path.expanduser("~"), ".code_indexer", + "default" if not base_path else hashlib.md5(base_path.encode()).hexdigest()) + + self.settings_path = fallback_dir + if not os.path.exists(fallback_dir): + os.makedirs(fallback_dir, exist_ok=True) + + def ensure_settings_dir(self): + """Ensure settings directory exists""" + + try: + if not os.path.exists(self.settings_path): + # Create directory structure + os.makedirs(self.settings_path, exist_ok=True) + else: + pass + + # Check if directory is writable + if not os.access(self.settings_path, os.W_OK): + # If directory is not writable, use .code_indexer in project or home directory as fallback + if self.base_path and os.path.exists(self.base_path) and os.access(self.base_path, os.W_OK): + fallback_dir = os.path.join(self.base_path, ".code_indexer", + os.path.basename(self.settings_path)) + else: + fallback_dir = os.path.join(os.path.expanduser("~"), ".code_indexer", + os.path.basename(self.settings_path)) + + self.settings_path = fallback_dir + if not os.path.exists(fallback_dir): + os.makedirs(fallback_dir, exist_ok=True) + except Exception: + # If unable to create settings directory, use .code_indexer in project or home directory + if self.base_path and os.path.exists(self.base_path): + fallback_dir = os.path.join(self.base_path, ".code_indexer", + hashlib.md5(self.base_path.encode()).hexdigest()) + else: + fallback_dir = os.path.join(os.path.expanduser("~"), ".code_indexer", + "default" if not self.base_path else hashlib.md5(self.base_path.encode()).hexdigest()) + + self.settings_path = fallback_dir + if not os.path.exists(fallback_dir): + os.makedirs(fallback_dir, exist_ok=True) + + def get_config_path(self): + """Get the path to the configuration file""" + try: + path = os.path.join(self.settings_path, CONFIG_FILE) + # Ensure directory exists + os.makedirs(os.path.dirname(path), exist_ok=True) + return path + except Exception: + # If error occurs, use file in project or home directory as fallback + if self.base_path and os.path.exists(self.base_path): + return os.path.join(self.base_path, CONFIG_FILE) + else: + return os.path.join(os.path.expanduser("~"), CONFIG_FILE) + + + def _get_timestamp(self): + """Get current timestamp""" + return datetime.now().isoformat() + + def save_config(self, config): + """Save configuration data + + Args: + config (dict): Configuration data + """ + try: + config_path = self.get_config_path() + # Add timestamp + config['last_updated'] = self._get_timestamp() + + # Ensure directory exists + os.makedirs(os.path.dirname(config_path), exist_ok=True) + + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + + return config + except Exception: + return config + + def load_config(self): + """Load configuration data + + Returns: + dict: Configuration data, or empty dict if file doesn't exist + """ + # If skip_load is set, return empty dict directly + if self.skip_load: + return {} + + try: + config_path = self.get_config_path() + if os.path.exists(config_path): + try: + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + return config + except (json.JSONDecodeError, UnicodeDecodeError): + # If file is corrupted, return empty dict + return {} + else: + pass + return {} + except Exception: + return {} + + def save_index(self, index_data): + """Save code index in JSON format + + Args: + index_data: Index data as dictionary or JSON string + """ + try: + index_path = self.get_index_path() + + # Ensure directory exists + dir_path = os.path.dirname(index_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path, exist_ok=True) + + # Check if directory is writable + if not os.access(dir_path, os.W_OK): + # Use project or home directory as fallback + if self.base_path and os.path.exists(self.base_path): + index_path = os.path.join(self.base_path, INDEX_FILE) + else: + index_path = os.path.join(os.path.expanduser("~"), INDEX_FILE) + + + # Convert to JSON string if it's an object with to_json method + if hasattr(index_data, 'to_json'): + json_data = index_data.to_json() + elif isinstance(index_data, str): + json_data = index_data + else: + # Assume it's a dictionary and convert to JSON + json_data = json.dumps(index_data, indent=2, default=str) + + with open(index_path, 'w', encoding='utf-8') as f: + f.write(json_data) + + + except Exception: + # Try saving to project or home directory + try: + if self.base_path and os.path.exists(self.base_path): + fallback_path = os.path.join(self.base_path, INDEX_FILE) + else: + fallback_path = os.path.join(os.path.expanduser("~"), INDEX_FILE) + + + # Convert to JSON string if it's an object with to_json method + if hasattr(index_data, 'to_json'): + json_data = index_data.to_json() + elif isinstance(index_data, str): + json_data = index_data + else: + json_data = json.dumps(index_data, indent=2, default=str) + + with open(fallback_path, 'w', encoding='utf-8') as f: + f.write(json_data) + except Exception: + pass + def load_index(self): + """Load code index from JSON format + + Returns: + dict: Index data, or None if file doesn't exist or has errors + """ + # If skip_load is set, return None directly + if self.skip_load: + return None + + try: + index_path = self.get_index_path() + + if os.path.exists(index_path): + try: + with open(index_path, 'r', encoding='utf-8') as f: + index_data = json.load(f) + return index_data + except (json.JSONDecodeError, UnicodeDecodeError): + # If file is corrupted, return None + return None + except Exception: + return None + else: + # Try loading from project or home directory + if self.base_path and os.path.exists(self.base_path): + fallback_path = os.path.join(self.base_path, INDEX_FILE) + else: + fallback_path = os.path.join(os.path.expanduser("~"), INDEX_FILE) + if os.path.exists(fallback_path): + try: + with open(fallback_path, 'r', encoding='utf-8') as f: + index_data = json.load(f) + return index_data + except Exception: + pass + return None + except Exception: + return None + + + + def cleanup_legacy_files(self) -> None: + """Clean up any legacy index files found.""" + try: + legacy_files = [ + os.path.join(self.settings_path, "file_index.pickle"), + os.path.join(self.settings_path, "content_cache.pickle"), + os.path.join(self.settings_path, INDEX_FILE) # Legacy JSON + ] + + for legacy_file in legacy_files: + if os.path.exists(legacy_file): + try: + os.remove(legacy_file) + except Exception: + pass + except Exception: + pass + + def clear(self): + """Clear config and index files""" + try: + + if os.path.exists(self.settings_path): + # Check if directory is writable + if not os.access(self.settings_path, os.W_OK): + return + + # Delete specific files only (config.json and index.json) + files_to_delete = [CONFIG_FILE, INDEX_FILE] + + for filename in files_to_delete: + file_path = os.path.join(self.settings_path, filename) + try: + if os.path.isfile(file_path): + os.unlink(file_path) + except Exception: + pass + + else: + pass + except Exception: + pass + def get_stats(self): + """Get statistics for the settings directory + + Returns: + dict: Dictionary containing file sizes and update times + """ + try: + + stats = { + 'settings_path': self.settings_path, + 'exists': os.path.exists(self.settings_path), + 'is_directory': os.path.isdir(self.settings_path) if os.path.exists(self.settings_path) else False, + 'writable': os.access(self.settings_path, os.W_OK) if os.path.exists(self.settings_path) else False, + 'files': {}, + 'temp_dir': tempfile.gettempdir(), + 'base_path': self.base_path + } + + if stats['exists'] and stats['is_directory']: + try: + # Get all files in the directory + all_files = os.listdir(self.settings_path) + stats['all_files'] = all_files + + # Get details for specific files + for filename in [CONFIG_FILE, INDEX_FILE]: + file_path = os.path.join(self.settings_path, filename) + if os.path.exists(file_path): + try: + file_stats = os.stat(file_path) + stats['files'][filename] = { + 'path': file_path, + 'size_bytes': file_stats.st_size, + 'last_modified': datetime.fromtimestamp(file_stats.st_mtime).isoformat(), + 'readable': os.access(file_path, os.R_OK), + 'writable': os.access(file_path, os.W_OK) + } + except Exception as e: + stats['files'][filename] = { + 'path': file_path, + 'error': str(e) + } + except Exception as e: + stats['list_error'] = str(e) + + # Check fallback path + if self.base_path and os.path.exists(self.base_path): + fallback_dir = os.path.join(self.base_path, ".code_indexer") + else: + fallback_dir = os.path.join(os.path.expanduser("~"), ".code_indexer") + stats['fallback_path'] = fallback_dir + stats['fallback_exists'] = os.path.exists(fallback_dir) + stats['fallback_is_directory'] = os.path.isdir(fallback_dir) if os.path.exists(fallback_dir) else False + + return stats + except Exception as e: + return { + 'error': str(e), + 'settings_path': self.settings_path, + 'temp_dir': tempfile.gettempdir(), + 'base_path': self.base_path + } + + def get_search_tools_config(self): + """Get the configuration of available search tools. + + Returns: + dict: A dictionary containing the list of available tool names. + """ + return { + "available_tools": [s.name for s in self.available_strategies], + "preferred_tool": self.get_preferred_search_tool().name if self.available_strategies else None + } + + def get_preferred_search_tool(self) -> SearchStrategy | None: + """Get the preferred search tool based on availability and priority. + + Returns: + SearchStrategy: An instance of the preferred search strategy, or None. + """ + if not self.available_strategies: + self.refresh_available_strategies() + + return self.available_strategies[0] if self.available_strategies else None + + def refresh_available_strategies(self): + """ + Force a refresh of the available search tools list. + """ + + self.available_strategies = _get_available_strategies() + + + def get_file_watcher_config(self) -> dict: + """ + Get file watcher specific configuration. + + Returns: + dict: File watcher configuration with defaults + """ + config = self.load_config() + default_config = { + "enabled": True, + "debounce_seconds": 6.0, + "additional_exclude_patterns": [], + "monitored_extensions": [], # Empty = use all supported extensions + "exclude_patterns": [ + ".git", ".svn", ".hg", + "node_modules", "__pycache__", ".venv", "venv", + ".DS_Store", "Thumbs.db", + "dist", "build", "target", ".idea", ".vscode", + ".pytest_cache", ".coverage", ".tox", + "bin", "obj" + ] + } + + # Merge with loaded config + file_watcher_config = config.get("file_watcher", {}) + for key, default_value in default_config.items(): + if key not in file_watcher_config: + file_watcher_config[key] = default_value + + return file_watcher_config + + def update_file_watcher_config(self, updates: dict) -> None: + """ + Update file watcher configuration. + + Args: + updates: Dictionary of configuration updates + """ + config = self.load_config() + if "file_watcher" not in config: + config["file_watcher"] = self.get_file_watcher_config() + + config["file_watcher"].update(updates) + self.save_config(config) diff --git a/reference/code-index-mcp-master/src/code_index_mcp/search/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/search/__init__.py new file mode 100644 index 00000000..f230a118 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/search/__init__.py @@ -0,0 +1 @@ +"""Search strategies package.""" diff --git a/reference/code-index-mcp-master/src/code_index_mcp/search/ag.py b/reference/code-index-mcp-master/src/code_index_mcp/search/ag.py new file mode 100644 index 00000000..2810e919 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/search/ag.py @@ -0,0 +1,145 @@ +""" +Search Strategy for The Silver Searcher (ag) +""" +import shutil +import subprocess +from typing import Dict, List, Optional, Tuple + +from .base import SearchStrategy, parse_search_output, create_word_boundary_pattern, is_safe_regex_pattern + +class AgStrategy(SearchStrategy): + """Search strategy using 'The Silver Searcher' (ag) command-line tool.""" + + @property + def name(self) -> str: + """The name of the search tool.""" + return 'ag' + + def is_available(self) -> bool: + """Check if 'ag' command is available on the system.""" + return shutil.which('ag') is not None + + def search( + self, + pattern: str, + base_path: str, + case_sensitive: bool = True, + context_lines: int = 0, + file_pattern: Optional[str] = None, + fuzzy: bool = False, + regex: bool = False + ) -> Dict[str, List[Tuple[int, str]]]: + """ + Execute a search using The Silver Searcher (ag). + + Args: + pattern: The search pattern + base_path: Directory to search in + case_sensitive: Whether search is case sensitive + context_lines: Number of context lines to show + file_pattern: File pattern to filter + fuzzy: Enable word boundary matching (not true fuzzy search) + regex: Enable regex pattern matching + """ + # ag prints line numbers and groups by file by default, which is good. + # --noheading is used to be consistent with other tools' output format. + cmd = ['ag', '--noheading'] + + if not case_sensitive: + cmd.append('--ignore-case') + + # Prepare search pattern + search_pattern = pattern + + if regex: + # Use regex mode - check for safety first + if not is_safe_regex_pattern(pattern): + raise ValueError(f"Potentially unsafe regex pattern: {pattern}") + # Don't add --literal, use regex mode + elif fuzzy: + # Use word boundary pattern for partial matching + search_pattern = create_word_boundary_pattern(pattern) + else: + # Use literal string search + cmd.append('--literal') + + if context_lines > 0: + cmd.extend(['--before', str(context_lines)]) + cmd.extend(['--after', str(context_lines)]) + + if file_pattern: + # Convert glob pattern to regex pattern for ag's -G parameter + # ag's -G expects regex, not glob patterns + regex_pattern = file_pattern + if '*' in file_pattern and not file_pattern.startswith('^') and not file_pattern.endswith('$'): + # Convert common glob patterns to regex + if file_pattern.startswith('*.'): + # Pattern like "*.py" -> "\.py$" + extension = file_pattern[2:] # Remove "*." + regex_pattern = f'\\.{extension}$' + elif file_pattern.endswith('*'): + # Pattern like "test_*" -> "^test_.*" + prefix = file_pattern[:-1] # Remove "*" + regex_pattern = f'^{prefix}.*' + elif '*' in file_pattern: + # Pattern like "test_*.py" -> "^test_.*\.py$" + # First escape dots, then replace * with .* + regex_pattern = file_pattern.replace('.', '\\.') + regex_pattern = regex_pattern.replace('*', '.*') + if not regex_pattern.startswith('^'): + regex_pattern = '^' + regex_pattern + if not regex_pattern.endswith('$'): + regex_pattern = regex_pattern + '$' + + cmd.extend(['-G', regex_pattern]) + + processed_patterns = set() + exclude_dirs = getattr(self, 'exclude_dirs', []) + exclude_file_patterns = getattr(self, 'exclude_file_patterns', []) + + for directory in exclude_dirs: + normalized = directory.strip() + if not normalized or normalized in processed_patterns: + continue + cmd.extend(['--ignore', normalized]) + processed_patterns.add(normalized) + + for pattern in exclude_file_patterns: + normalized = pattern.strip() + if not normalized or normalized in processed_patterns: + continue + if normalized.startswith('!'): + normalized = normalized[1:] + cmd.extend(['--ignore', normalized]) + processed_patterns.add(normalized) + + # Add -- to treat pattern as a literal argument, preventing injection + cmd.append('--') + cmd.append(search_pattern) + cmd.append('.') # Use current directory since we set cwd=base_path + + try: + # ag exits with 1 if no matches are found, which is not an error. + # It exits with 0 on success (match found). Other codes are errors. + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + errors='replace', + check=False, # Do not raise CalledProcessError on non-zero exit + cwd=base_path # Set working directory to project base path for proper pattern resolution + ) + # We don't check returncode > 1 because ag's exit code behavior + # is less standardized than rg/ug. 0 for match, 1 for no match. + # Any actual error will likely raise an exception or be in stderr. + if process.returncode > 1: + raise RuntimeError(f"ag failed with exit code {process.returncode}: {process.stderr}") + + return parse_search_output(process.stdout, base_path) + + except FileNotFoundError: + raise RuntimeError("'ag' (The Silver Searcher) not found. Please install it and ensure it's in your PATH.") + except Exception as e: + # Re-raise other potential exceptions like permission errors + raise RuntimeError(f"An error occurred while running ag: {e}") diff --git a/reference/code-index-mcp-master/src/code_index_mcp/search/base.py b/reference/code-index-mcp-master/src/code_index_mcp/search/base.py new file mode 100644 index 00000000..274cb7da --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/search/base.py @@ -0,0 +1,234 @@ +""" +Search Strategies for Code Indexer + +This module defines the abstract base class for search strategies and will contain +concrete implementations for different search tools like ugrep, ripgrep, etc. +""" +import os +import re +import shutil +import subprocess +import sys +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING + +from ..indexing.qualified_names import normalize_file_path + +if TYPE_CHECKING: # pragma: no cover + from ..utils.file_filter import FileFilter + +def parse_search_output( + output: str, + base_path: str +) -> Dict[str, List[Tuple[int, str]]]: + """ + Parse the output of command-line search tools (grep, ag, rg). + + Args: + output: The raw output from the command-line tool. + base_path: The base path of the project to make file paths relative. + + Returns: + A dictionary where keys are file paths and values are lists of (line_number, line_content) tuples. + """ + results = {} + # Normalize base_path to ensure consistent path separation + normalized_base_path = os.path.normpath(base_path) + + for line in output.strip().split('\n'): + if not line.strip(): + continue + try: + # Try to parse as a matched line first (format: path:linenum:content) + parts = line.split(':', 2) + + # Check if this might be a context line (format: path-linenum-content) + # Context lines use '-' as separator in grep/ag output + if len(parts) < 3 and '-' in line: + # Try to parse as context line + # Match pattern: path-linenum-content or path-linenum-\tcontent + match = re.match(r'^(.*?)-(\d+)[-\t](.*)$', line) + if match: + file_path_abs = match.group(1) + line_number_str = match.group(2) + content = match.group(3) + else: + # If regex doesn't match, skip this line + continue + elif sys.platform == "win32" and len(parts) >= 3 and len(parts[0]) == 1 and parts[1].startswith('\\'): + # Handle Windows paths with drive letter (e.g., C:\path\file.txt) + file_path_abs = f"{parts[0]}:{parts[1]}" + line_number_str = parts[2].split(':', 1)[0] + content = parts[2].split(':', 1)[1] if ':' in parts[2] else parts[2] + elif len(parts) >= 3: + # Standard format: path:linenum:content + file_path_abs = parts[0] + line_number_str = parts[1] + content = parts[2] + else: + # Line doesn't match any expected format + continue + + line_number = int(line_number_str) + + # If the path is already relative (doesn't start with /), keep it as is + # Otherwise, make it relative to the base_path + if os.path.isabs(file_path_abs): + relative_path = os.path.relpath(file_path_abs, normalized_base_path) + else: + # Path is already relative, use it as is + relative_path = file_path_abs + + # Normalize path separators for consistency + relative_path = normalize_file_path(relative_path) + + if relative_path not in results: + results[relative_path] = [] + results[relative_path].append((line_number, content)) + except (ValueError, IndexError): + # Silently ignore lines that don't match the expected format + # This can happen with summary lines or other tool-specific output + pass + + return results + + +def create_word_boundary_pattern(pattern: str) -> str: + """ + Create word boundary patterns for partial matching. + This is NOT true fuzzy search, but allows matching words at boundaries. + + Args: + pattern: Original search pattern + + Returns: + Word boundary pattern for regex matching + """ + # Escape any regex special characters to make them literal + escaped = re.escape(pattern) + + # Create word boundary pattern that matches: + # 1. Word at start of word boundary (e.g., "test" in "testing") + # 2. Word at end of word boundary (e.g., "test" in "mytest") + # 3. Whole word (e.g., "test" as standalone word) + if len(pattern) >= 3: # Only for patterns of reasonable length + # This pattern allows partial matches at word boundaries + boundary_pattern = f"\\b{escaped}|{escaped}\\b" + else: + # For short patterns, require full word boundaries to avoid too many matches + boundary_pattern = f"\\b{escaped}\\b" + + return boundary_pattern + + +def is_safe_regex_pattern(pattern: str) -> bool: + """ + Check if a pattern appears to be a safe regex pattern. + + Args: + pattern: The search pattern to check + + Returns: + True if the pattern looks like a safe regex, False otherwise + """ + # Strong indicators of regex intent + strong_regex_indicators = ['|', '(', ')', '[', ']', '^', '$'] + + # Weaker indicators that need context + weak_regex_indicators = ['.', '*', '+', '?'] + + # Check for strong regex indicators + has_strong_regex = any(char in pattern for char in strong_regex_indicators) + + # Check for weak indicators with context + has_weak_regex = any(char in pattern for char in weak_regex_indicators) + + # If has strong indicators, likely regex + if has_strong_regex: + # Still check for dangerous patterns + dangerous_patterns = [ + r'(.+)+', # Nested quantifiers + r'(.*)*', # Nested stars + r'(.{0,})+', # Potential ReDoS patterns + ] + + has_dangerous_patterns = any(dangerous in pattern for dangerous in dangerous_patterns) + return not has_dangerous_patterns + + # If only weak indicators, need more context + if has_weak_regex: + # Patterns like ".*", ".+", "file.*py" look like regex + # But "file.txt", "test.py" look like literal filenames + regex_like_patterns = [ + r'\.\*', # .* + r'\.\+', # .+ + r'\.\w*\*', # .something* + r'\*\.', # *. + r'\w+\.\*\w*', # word.*word + ] + + return any(re.search(regex_pattern, pattern) for regex_pattern in regex_like_patterns) + + return False + + +class SearchStrategy(ABC): + """ + Abstract base class for a search strategy. + + Each strategy is responsible for searching code using a specific tool or method. + """ + + def configure_excludes(self, file_filter: Optional['FileFilter']) -> None: + """Configure shared exclusion settings for the strategy.""" + self.file_filter = file_filter + if file_filter: + self.exclude_dirs = sorted(set(file_filter.exclude_dirs)) + self.exclude_file_patterns = sorted(set(file_filter.exclude_files)) + else: + self.exclude_dirs = [] + self.exclude_file_patterns = [] + + @property + @abstractmethod + def name(self) -> str: + """The name of the search tool (e.g., 'ugrep', 'ripgrep').""" + pass + + @abstractmethod + def is_available(self) -> bool: + """ + Check if the search tool for this strategy is available on the system. + + Returns: + True if the tool is available, False otherwise. + """ + pass + + @abstractmethod + def search( + self, + pattern: str, + base_path: str, + case_sensitive: bool = True, + context_lines: int = 0, + file_pattern: Optional[str] = None, + fuzzy: bool = False, + regex: bool = False + ) -> Dict[str, List[Tuple[int, str]]]: + """ + Execute a search using the specific strategy. + + Args: + pattern: The search pattern. + base_path: The root directory to search in. + case_sensitive: Whether the search is case-sensitive. + context_lines: Number of context lines to show around each match. + file_pattern: Glob pattern to filter files (e.g., "*.py"). + fuzzy: Whether to enable fuzzy/partial matching. + regex: Whether to enable regex pattern matching. + + Returns: + A dictionary mapping filenames to lists of (line_number, line_content) tuples. + """ + pass diff --git a/reference/code-index-mcp-master/src/code_index_mcp/search/basic.py b/reference/code-index-mcp-master/src/code_index_mcp/search/basic.py new file mode 100644 index 00000000..76039790 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/search/basic.py @@ -0,0 +1,116 @@ +""" +Basic, pure-Python search strategy. +""" +import fnmatch +import os +import re +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +from .base import SearchStrategy, create_word_boundary_pattern, is_safe_regex_pattern + +class BasicSearchStrategy(SearchStrategy): + """ + A basic, pure-Python search strategy. + + This strategy iterates through files and lines manually. It's a fallback + for when no advanced command-line search tools are available. + It does not support context lines. + """ + + @property + def name(self) -> str: + """The name of the search tool.""" + return 'basic' + + def is_available(self) -> bool: + """This basic strategy is always available.""" + return True + + def _matches_pattern(self, filename: str, pattern: str) -> bool: + """Check if filename matches the glob pattern.""" + if not pattern: + return True + + # Handle simple cases efficiently + if pattern.startswith('*') and not any(c in pattern[1:] for c in '*?[]{}'): + return filename.endswith(pattern[1:]) + + # Use fnmatch for more complex patterns + return fnmatch.fnmatch(filename, pattern) + + def search( + self, + pattern: str, + base_path: str, + case_sensitive: bool = True, + context_lines: int = 0, + file_pattern: Optional[str] = None, + fuzzy: bool = False, + regex: bool = False + ) -> Dict[str, List[Tuple[int, str]]]: + """ + Execute a basic, line-by-line search. + + Note: This implementation does not support context_lines. + Args: + pattern: The search pattern + base_path: Directory to search in + case_sensitive: Whether search is case sensitive + context_lines: Number of context lines (not supported) + file_pattern: File pattern to filter + fuzzy: Enable word boundary matching + regex: Enable regex pattern matching + """ + results: Dict[str, List[Tuple[int, str]]] = {} + + flags = 0 if case_sensitive else re.IGNORECASE + + try: + if regex: + # Use regex mode - check for safety first + if not is_safe_regex_pattern(pattern): + raise ValueError(f"Potentially unsafe regex pattern: {pattern}") + search_regex = re.compile(pattern, flags) + elif fuzzy: + # Use word boundary pattern for partial matching + search_pattern = create_word_boundary_pattern(pattern) + search_regex = re.compile(search_pattern, flags) + else: + # Use literal string search + search_regex = re.compile(re.escape(pattern), flags) + except re.error as e: + raise ValueError(f"Invalid regex pattern: {pattern}, error: {e}") + + file_filter = getattr(self, 'file_filter', None) + base = Path(base_path) + + for root, dirs, files in os.walk(base_path): + if file_filter: + dirs[:] = [d for d in dirs if not file_filter.should_exclude_directory(d)] + + for file in files: + if file_pattern and not self._matches_pattern(file, file_pattern): + continue + + file_path = Path(root) / file + + if file_filter and not file_filter.should_process_path(file_path, base): + continue + + rel_path = os.path.relpath(file_path, base_path) + + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + for line_num, line in enumerate(f, 1): + if search_regex.search(line): + content = line.rstrip('\n') + if rel_path not in results: + results[rel_path] = [] + results[rel_path].append((line_num, content)) + except (UnicodeDecodeError, PermissionError, OSError): + continue + except Exception: + continue + + return results diff --git a/reference/code-index-mcp-master/src/code_index_mcp/search/grep.py b/reference/code-index-mcp-master/src/code_index_mcp/search/grep.py new file mode 100644 index 00000000..3c6dfd7d --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/search/grep.py @@ -0,0 +1,131 @@ +""" +Search Strategy for standard grep +""" +import shutil +import subprocess +from typing import Dict, List, Optional, Tuple + +from .base import SearchStrategy, parse_search_output, create_word_boundary_pattern, is_safe_regex_pattern + +class GrepStrategy(SearchStrategy): + """ + Search strategy using the standard 'grep' command-line tool. + + This is intended as a fallback for when more advanced tools like + ugrep, ripgrep, or ag are not available. + """ + + @property + def name(self) -> str: + """The name of the search tool.""" + return 'grep' + + def is_available(self) -> bool: + """Check if 'grep' command is available on the system.""" + return shutil.which('grep') is not None + + def search( + self, + pattern: str, + base_path: str, + case_sensitive: bool = True, + context_lines: int = 0, + file_pattern: Optional[str] = None, + fuzzy: bool = False, + regex: bool = False + ) -> Dict[str, List[Tuple[int, str]]]: + """ + Execute a search using standard grep. + + Args: + pattern: The search pattern + base_path: Directory to search in + case_sensitive: Whether search is case sensitive + context_lines: Number of context lines to show + file_pattern: File pattern to filter + fuzzy: Enable word boundary matching + regex: Enable regex pattern matching + """ + # -r: recursive, -n: line number + cmd = ['grep', '-r', '-n'] + + # Prepare search pattern + search_pattern = pattern + + if regex: + # Use regex mode - check for safety first + if not is_safe_regex_pattern(pattern): + raise ValueError(f"Potentially unsafe regex pattern: {pattern}") + cmd.append('-E') # Extended Regular Expressions + elif fuzzy: + # Use word boundary pattern for partial matching + search_pattern = create_word_boundary_pattern(pattern) + cmd.append('-E') # Extended Regular Expressions + else: + # Auto-detect if pattern looks like a safe regex + if is_safe_regex_pattern(pattern): + # Pattern contains regex chars, use extended regex mode + cmd.append('-E') + else: + # Use literal string search + cmd.append('-F') + + if not case_sensitive: + cmd.append('-i') + + if context_lines > 0: + cmd.extend(['-A', str(context_lines)]) + cmd.extend(['-B', str(context_lines)]) + + if file_pattern: + # Note: grep's --include uses glob patterns, not regex + cmd.append(f'--include={file_pattern}') + + exclude_dirs = getattr(self, 'exclude_dirs', []) + exclude_file_patterns = getattr(self, 'exclude_file_patterns', []) + + processed_dirs = set() + for directory in exclude_dirs: + normalized = directory.strip() + if not normalized or normalized in processed_dirs: + continue + cmd.append(f'--exclude-dir={normalized}') + processed_dirs.add(normalized) + + processed_files = set() + for pattern in exclude_file_patterns: + normalized = pattern.strip() + if not normalized or normalized in processed_files: + continue + if normalized.startswith('!'): + normalized = normalized[1:] + cmd.append(f'--exclude={normalized}') + processed_files.add(normalized) + + # Add -- to treat pattern as a literal argument, preventing injection + cmd.append('--') + cmd.append(search_pattern) + cmd.append('.') # Use current directory since we set cwd=base_path + + try: + # grep exits with 1 if no matches are found, which is not an error. + # It exits with 0 on success (match found). >1 for errors. + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + errors='replace', + check=False, + cwd=base_path # Set working directory to project base path for proper pattern resolution + ) + + if process.returncode > 1: + raise RuntimeError(f"grep failed with exit code {process.returncode}: {process.stderr}") + + return parse_search_output(process.stdout, base_path) + + except FileNotFoundError: + raise RuntimeError("'grep' not found. Please install it and ensure it's in your PATH.") + except Exception as e: + raise RuntimeError(f"An error occurred while running grep: {e}") diff --git a/reference/code-index-mcp-master/src/code_index_mcp/search/ripgrep.py b/reference/code-index-mcp-master/src/code_index_mcp/search/ripgrep.py new file mode 100644 index 00000000..c36b0e0b --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/search/ripgrep.py @@ -0,0 +1,121 @@ +""" +Search Strategy for ripgrep +""" +import shutil +import subprocess +from typing import Dict, List, Optional, Tuple + +from .base import SearchStrategy, parse_search_output, create_word_boundary_pattern, is_safe_regex_pattern + +class RipgrepStrategy(SearchStrategy): + """Search strategy using the 'ripgrep' (rg) command-line tool.""" + + @property + def name(self) -> str: + """The name of the search tool.""" + return 'ripgrep' + + def is_available(self) -> bool: + """Check if 'rg' command is available on the system.""" + return shutil.which('rg') is not None + + def search( + self, + pattern: str, + base_path: str, + case_sensitive: bool = True, + context_lines: int = 0, + file_pattern: Optional[str] = None, + fuzzy: bool = False, + regex: bool = False + ) -> Dict[str, List[Tuple[int, str]]]: + """ + Execute a search using ripgrep. + + Args: + pattern: The search pattern + base_path: Directory to search in + case_sensitive: Whether search is case sensitive + context_lines: Number of context lines to show + file_pattern: File pattern to filter + fuzzy: Enable word boundary matching (not true fuzzy search) + regex: Enable regex pattern matching + """ + cmd = ['rg', '--line-number', '--no-heading', '--color=never', '--no-ignore'] + + if not case_sensitive: + cmd.append('--ignore-case') + + # Prepare search pattern + search_pattern = pattern + + if regex: + # Use regex mode - check for safety first + if not is_safe_regex_pattern(pattern): + raise ValueError(f"Potentially unsafe regex pattern: {pattern}") + # Don't add --fixed-strings, use regex mode + elif fuzzy: + # Use word boundary pattern for partial matching + search_pattern = create_word_boundary_pattern(pattern) + else: + # Use literal string search + cmd.append('--fixed-strings') + + if context_lines > 0: + cmd.extend(['--context', str(context_lines)]) + + if file_pattern: + cmd.extend(['--glob', file_pattern]) + + exclude_dirs = getattr(self, 'exclude_dirs', []) + exclude_file_patterns = getattr(self, 'exclude_file_patterns', []) + + processed_patterns = set() + + for directory in exclude_dirs: + normalized = directory.strip() + if not normalized or normalized in processed_patterns: + continue + cmd.extend(['--glob', f'!**/{normalized}/**']) + processed_patterns.add(normalized) + + for pattern in exclude_file_patterns: + normalized = pattern.strip() + if not normalized or normalized in processed_patterns: + continue + if normalized.startswith('!'): + glob_pattern = normalized + elif any(ch in normalized for ch in '*?[') or '/' in normalized: + glob_pattern = f'!{normalized}' + else: + glob_pattern = f'!**/{normalized}' + cmd.extend(['--glob', glob_pattern]) + processed_patterns.add(normalized) + + # Add -- to treat pattern as a literal argument, preventing injection + cmd.append('--') + cmd.append(search_pattern) + cmd.append('.') # Use current directory since we set cwd=base_path + + try: + # ripgrep exits with 1 if no matches are found, which is not an error. + # It exits with 2 for actual errors. + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + errors='replace', + check=False, # Do not raise CalledProcessError on non-zero exit + cwd=base_path # Set working directory to project base path for proper glob resolution + ) + if process.returncode > 1: + raise RuntimeError(f"ripgrep failed with exit code {process.returncode}: {process.stderr}") + + return parse_search_output(process.stdout, base_path) + + except FileNotFoundError: + raise RuntimeError("ripgrep (rg) not found. Please install it and ensure it's in your PATH.") + except Exception as e: + # Re-raise other potential exceptions like permission errors + raise RuntimeError(f"An error occurred while running ripgrep: {e}") diff --git a/reference/code-index-mcp-master/src/code_index_mcp/search/ugrep.py b/reference/code-index-mcp-master/src/code_index_mcp/search/ugrep.py new file mode 100644 index 00000000..a1176ae1 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/search/ugrep.py @@ -0,0 +1,121 @@ +""" +Search Strategy for ugrep +""" +import shutil +import subprocess +from typing import Dict, List, Optional, Tuple + +from .base import SearchStrategy, parse_search_output, create_word_boundary_pattern, is_safe_regex_pattern + +class UgrepStrategy(SearchStrategy): + """Search strategy using the 'ugrep' (ug) command-line tool.""" + + @property + def name(self) -> str: + """The name of the search tool.""" + return 'ugrep' + + def is_available(self) -> bool: + """Check if 'ug' command is available on the system.""" + return shutil.which('ug') is not None + + def search( + self, + pattern: str, + base_path: str, + case_sensitive: bool = True, + context_lines: int = 0, + file_pattern: Optional[str] = None, + fuzzy: bool = False, + regex: bool = False + ) -> Dict[str, List[Tuple[int, str]]]: + """ + Execute a search using the 'ug' command-line tool. + + Args: + pattern: The search pattern + base_path: Directory to search in + case_sensitive: Whether search is case sensitive + context_lines: Number of context lines to show + file_pattern: File pattern to filter + fuzzy: Enable true fuzzy search (ugrep native support) + regex: Enable regex pattern matching + """ + if not self.is_available(): + return {"error": "ugrep (ug) command not found."} + + cmd = ['ug', '-r', '--line-number', '--no-heading'] + + if fuzzy: + # ugrep has native fuzzy search support + cmd.append('--fuzzy') + elif regex: + # Use regex mode - check for safety first + if not is_safe_regex_pattern(pattern): + raise ValueError(f"Potentially unsafe regex pattern: {pattern}") + # Don't add --fixed-strings, use regex mode + else: + # Use literal string search + cmd.append('--fixed-strings') + + if not case_sensitive: + cmd.append('--ignore-case') + + if context_lines > 0: + cmd.extend(['-A', str(context_lines), '-B', str(context_lines)]) + + if file_pattern: + cmd.extend(['--include', file_pattern]) + + processed_patterns = set() + exclude_dirs = getattr(self, 'exclude_dirs', []) + exclude_file_patterns = getattr(self, 'exclude_file_patterns', []) + + for directory in exclude_dirs: + normalized = directory.strip() + if not normalized or normalized in processed_patterns: + continue + cmd.extend(['--ignore', f'**/{normalized}/**']) + processed_patterns.add(normalized) + + for pattern in exclude_file_patterns: + normalized = pattern.strip() + if not normalized or normalized in processed_patterns: + continue + if normalized.startswith('!'): + ignore_pattern = normalized[1:] + elif any(ch in normalized for ch in '*?[') or '/' in normalized: + ignore_pattern = normalized + else: + ignore_pattern = f'**/{normalized}' + cmd.extend(['--ignore', ignore_pattern]) + processed_patterns.add(normalized) + + # Add '--' to treat pattern as a literal argument, preventing injection + cmd.append('--') + cmd.append(pattern) + cmd.append('.') # Use current directory since we set cwd=base_path + + try: + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + errors='ignore', # Ignore decoding errors for binary-like content + check=False, # Do not raise exception on non-zero exit codes + cwd=base_path # Set working directory to project base path for proper pattern resolution + ) + + # ugrep exits with 1 if no matches are found, which is not an error for us. + # It exits with 2 for actual errors. + if process.returncode > 1: + error_output = process.stderr.strip() + return {"error": f"ugrep execution failed with code {process.returncode}", "details": error_output} + + return parse_search_output(process.stdout, base_path) + + except FileNotFoundError: + return {"error": "ugrep (ug) command not found. Please ensure it's installed and in your PATH."} + except Exception as e: + return {"error": f"An unexpected error occurred during search: {str(e)}"} diff --git a/reference/code-index-mcp-master/src/code_index_mcp/server.py b/reference/code-index-mcp-master/src/code_index_mcp/server.py new file mode 100644 index 00000000..6e910355 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/server.py @@ -0,0 +1,386 @@ +""" +Code Index MCP Server + +This MCP server allows LLMs to index, search, and analyze code from a project directory. +It provides tools for file discovery, content retrieval, and code analysis. + +This version uses a service-oriented architecture where MCP decorators delegate +to domain-specific services for business logic. +""" + +# Standard library imports +import argparse +import inspect +import sys +import logging +from contextlib import asynccontextmanager +from dataclasses import dataclass +from typing import AsyncIterator, Dict, Any, List, Optional +from urllib.parse import unquote + +# Third-party imports +from mcp.server.fastmcp import FastMCP, Context + +# Local imports +from .project_settings import ProjectSettings +from .services import ( + SearchService, FileService, SettingsService, FileWatcherService +) +from .services.settings_service import manage_temp_directory +from .services.file_discovery_service import FileDiscoveryService +from .services.project_management_service import ProjectManagementService +from .services.index_management_service import IndexManagementService +from .services.code_intelligence_service import CodeIntelligenceService +from .services.system_management_service import SystemManagementService +from .utils import handle_mcp_tool_errors + +# Setup logging without writing to files +def setup_indexing_performance_logging(): + """Setup logging (stderr only); remove any file-based logging.""" + + root_logger = logging.getLogger() + root_logger.handlers.clear() + + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + + # stderr for errors only + stderr_handler = logging.StreamHandler(sys.stderr) + stderr_handler.setFormatter(formatter) + stderr_handler.setLevel(logging.ERROR) + + root_logger.addHandler(stderr_handler) + root_logger.setLevel(logging.DEBUG) + +# Initialize logging (no file handlers) +setup_indexing_performance_logging() +logger = logging.getLogger(__name__) + +@dataclass +class CodeIndexerContext: + """Context for the Code Indexer MCP server.""" + base_path: str + settings: ProjectSettings + file_count: int = 0 + file_watcher_service: FileWatcherService = None + + +@dataclass +class _CLIConfig: + """Holds CLI configuration for bootstrap operations.""" + project_path: str | None = None + + +class _BootstrapRequestContext: + """Minimal request context to reuse business services during bootstrap.""" + + def __init__(self, lifespan_context: CodeIndexerContext): + self.lifespan_context = lifespan_context + self.session = None + self.meta = None + + +_CLI_CONFIG = _CLIConfig() + +@asynccontextmanager +async def indexer_lifespan(_server: FastMCP) -> AsyncIterator[CodeIndexerContext]: + """Manage the lifecycle of the Code Indexer MCP server.""" + # Don't set a default path, user must explicitly set project path + base_path = "" # Empty string to indicate no path is set + + # Initialize settings manager with skip_load=True to skip loading files + settings = ProjectSettings(base_path, skip_load=True) + + # Initialize context - file watcher will be initialized later when project path is set + context = CodeIndexerContext( + base_path=base_path, + settings=settings, + file_watcher_service=None + ) + + try: + # Bootstrap project path when provided via CLI. + if _CLI_CONFIG.project_path: + bootstrap_ctx = Context( + request_context=_BootstrapRequestContext(context), + fastmcp=mcp + ) + try: + message = ProjectManagementService(bootstrap_ctx).initialize_project( + _CLI_CONFIG.project_path + ) + logger.info("Project initialized from CLI flag: %s", message) + except Exception as exc: # pylint: disable=broad-except + logger.error("Failed to initialize project from CLI flag: %s", exc) + raise RuntimeError( + f"Failed to initialize project path '{_CLI_CONFIG.project_path}'" + ) from exc + + # Provide context to the server + yield context + finally: + # Stop file watcher if it was started + if context.file_watcher_service: + context.file_watcher_service.stop_monitoring() + +# Create the MCP server with lifespan manager +mcp = FastMCP("CodeIndexer", lifespan=indexer_lifespan, dependencies=["pathlib"]) + +# ----- RESOURCES ----- + +@mcp.resource("files://{file_path}") +def get_file_content(file_path: str) -> str: + """Get the content of a specific file.""" + decoded_path = unquote(file_path) + ctx = mcp.get_context() + return FileService(ctx).get_file_content(decoded_path) + +# ----- TOOLS ----- + +@mcp.tool() +@handle_mcp_tool_errors(return_type='str') +def set_project_path(path: str, ctx: Context) -> str: + """Set the base project path for indexing.""" + return ProjectManagementService(ctx).initialize_project(path) + +@mcp.tool() +@handle_mcp_tool_errors(return_type='dict') +def search_code_advanced( + pattern: str, + ctx: Context, + case_sensitive: bool = True, + context_lines: int = 0, + file_pattern: str = None, + fuzzy: bool = False, + regex: bool = None, + start_index: int = 0, + max_results: Optional[int] = 10 +) -> Dict[str, Any]: + """ + Search for a code pattern in the project using an advanced, fast tool with pagination support. + + This tool automatically selects the best available command-line search tool + (like ugrep, ripgrep, ag, or grep) for maximum performance. + + Args: + pattern: The search pattern. Can be literal text or regex (see regex parameter). + case_sensitive: Whether the search should be case-sensitive. + context_lines: Number of lines to show before and after the match. + file_pattern: A glob pattern to filter files to search in + (e.g., "*.py", "*.js", "test_*.py"). + All search tools now handle glob patterns consistently: + - ugrep: Uses glob patterns (*.py, *.{js,ts}) + - ripgrep: Uses glob patterns (*.py, *.{js,ts}) + - ag (Silver Searcher): Automatically converts globs to regex patterns + - grep: Basic glob pattern matching + All common glob patterns like "*.py", "test_*.js", "src/*.ts" are supported. + fuzzy: If True, enables fuzzy/partial matching behavior varies by search tool: + - ugrep: Native fuzzy search with --fuzzy flag (true edit-distance fuzzy search) + - ripgrep, ag, grep, basic: Word boundary pattern matching (not true fuzzy search) + IMPORTANT: Only ugrep provides true fuzzy search. Other tools use word boundary + matching which allows partial matches at word boundaries. + For exact literal matches, set fuzzy=False (default and recommended). + regex: Controls regex pattern matching behavior: + - If True, enables regex pattern matching + - If False, forces literal string search + - If None (default), automatically detects regex patterns and enables regex for patterns like "ERROR|WARN" + The pattern will always be validated for safety to prevent ReDoS attacks. + start_index: Zero-based offset into the flattened match list. Use to fetch subsequent pages. + max_results: Maximum number of matches to return (default 10). Pass None to retrieve all matches. + + Returns: + A dictionary containing: + - results: List of matches with file, line, and text keys. + - pagination: Metadata with total_matches, returned, start_index, end_index, has_more, + and optionally max_results. + If an error occurs, an error message is returned instead. + + """ + return SearchService(ctx).search_code( + pattern=pattern, + case_sensitive=case_sensitive, + context_lines=context_lines, + file_pattern=file_pattern, + fuzzy=fuzzy, + regex=regex, + start_index=start_index, + max_results=max_results + ) + +@mcp.tool() +@handle_mcp_tool_errors(return_type='list') +def find_files(pattern: str, ctx: Context) -> List[str]: + """ + Find files matching a glob pattern using pre-built file index. + + Use when: + - Looking for files by pattern (e.g., "*.py", "test_*.js") + - Searching by filename only (e.g., "README.md" finds all README files) + - Checking if specific files exist in the project + - Getting file lists for further analysis + + Pattern matching: + - Supports both full path and filename-only matching + - Uses standard glob patterns (*, ?, []) + - Fast lookup using in-memory file index + - Uses forward slashes consistently across all platforms + + Args: + pattern: Glob pattern to match files (e.g., "*.py", "test_*.js", "README.md") + + Returns: + List of file paths matching the pattern + """ + return FileDiscoveryService(ctx).find_files(pattern) + +@mcp.tool() +@handle_mcp_tool_errors(return_type='dict') +def get_file_summary(file_path: str, ctx: Context) -> Dict[str, Any]: + """ + Get a summary of a specific file, including: + - Line count + - Function/class definitions (for supported languages) + - Import statements + - Basic complexity metrics + """ + return CodeIntelligenceService(ctx).analyze_file(file_path) + +@mcp.tool() +@handle_mcp_tool_errors(return_type='str') +def refresh_index(ctx: Context) -> str: + """ + Manually refresh the project index when files have been added/removed/moved. + + Use when: + - File watcher is disabled or unavailable + - After large-scale operations (git checkout, merge, pull) that change many files + - When you want immediate index rebuild without waiting for file watcher debounce + - When find_files results seem incomplete or outdated + - For troubleshooting suspected index synchronization issues + + Important notes for LLMs: + - Always available as backup when file watcher is not working + - Performs full project re-indexing for complete accuracy + - Use when you suspect the index is stale after file system changes + - **Call this after programmatic file modifications if file watcher seems unresponsive** + - Complements the automatic file watcher system + + Returns: + Success message with total file count + """ + return IndexManagementService(ctx).rebuild_index() + +@mcp.tool() +@handle_mcp_tool_errors(return_type='str') +def build_deep_index(ctx: Context) -> str: + """ + Build the deep index (full symbol extraction) for the current project. + + This performs a complete re-index and loads it into memory. + """ + return IndexManagementService(ctx).rebuild_deep_index() + +@mcp.tool() +@handle_mcp_tool_errors(return_type='dict') +def get_settings_info(ctx: Context) -> Dict[str, Any]: + """Get information about the project settings.""" + return SettingsService(ctx).get_settings_info() + +@mcp.tool() +@handle_mcp_tool_errors(return_type='dict') +def create_temp_directory() -> Dict[str, Any]: + """Create the temporary directory used for storing index data.""" + return manage_temp_directory('create') + +@mcp.tool() +@handle_mcp_tool_errors(return_type='dict') +def check_temp_directory() -> Dict[str, Any]: + """Check the temporary directory used for storing index data.""" + return manage_temp_directory('check') + +@mcp.tool() +@handle_mcp_tool_errors(return_type='str') +def clear_settings(ctx: Context) -> str: + """Clear all settings and cached data.""" + return SettingsService(ctx).clear_all_settings() + +@mcp.tool() +@handle_mcp_tool_errors(return_type='str') +def refresh_search_tools(ctx: Context) -> str: + """ + Manually re-detect the available command-line search tools on the system. + This is useful if you have installed a new tool (like ripgrep) after starting the server. + """ + return SearchService(ctx).refresh_search_tools() + +@mcp.tool() +@handle_mcp_tool_errors(return_type='dict') +def get_file_watcher_status(ctx: Context) -> Dict[str, Any]: + """Get file watcher service status and statistics.""" + return SystemManagementService(ctx).get_file_watcher_status() + +@mcp.tool() +@handle_mcp_tool_errors(return_type='str') +def configure_file_watcher( + ctx: Context, + enabled: bool = None, + debounce_seconds: float = None, + additional_exclude_patterns: list = None +) -> str: + """Configure file watcher service settings.""" + return SystemManagementService(ctx).configure_file_watcher(enabled, debounce_seconds, additional_exclude_patterns) + +# ----- PROMPTS ----- +# Removed: analyze_code, code_search, set_project prompts + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse CLI arguments for the MCP server.""" + parser = argparse.ArgumentParser(description="Code Index MCP server") + parser.add_argument( + "--project-path", + dest="project_path", + help="Set the project path on startup (equivalent to calling set_project_path)." + ) + parser.add_argument( + "--transport", + choices=["stdio", "sse", "streamable-http"], + default="stdio", + help="Transport protocol to use (default: stdio)." + ) + parser.add_argument( + "--mount-path", + dest="mount_path", + default=None, + help="Mount path when using SSE transport." + ) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None): + """Main function to run the MCP server.""" + args = _parse_args(argv) + + # Store CLI configuration for lifespan bootstrap. + _CLI_CONFIG.project_path = args.project_path + + run_kwargs = {"transport": args.transport} + if args.transport == "sse" and args.mount_path: + run_signature = inspect.signature(mcp.run) + if "mount_path" in run_signature.parameters: + run_kwargs["mount_path"] = args.mount_path + else: + logger.warning( + "Ignoring --mount-path because this FastMCP version " + "does not accept the parameter." + ) + + try: + mcp.run(**run_kwargs) + except RuntimeError as exc: + logger.error("MCP server terminated with error: %s", exc) + raise SystemExit(1) from exc + except Exception as exc: # pylint: disable=broad-except + logger.error("Unexpected MCP server error: %s", exc) + raise + +if __name__ == '__main__': + main() diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/services/__init__.py new file mode 100644 index 00000000..7694446f --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/__init__.py @@ -0,0 +1,48 @@ +""" +Service layer for the Code Index MCP server. + +This package contains domain-specific services that handle the business logic +for different areas of functionality: + + +- SearchService: Code search operations and search tool management +- FileService: File operations, content retrieval, and analysis +- SettingsService: Settings management and directory operations + +Each service follows a consistent pattern: +- Constructor accepts MCP Context parameter +- Methods correspond to MCP entry points +- Clear domain boundaries with no cross-service dependencies +- Shared utilities accessed through utils module +- Meaningful exceptions raised for error conditions +""" + +# New Three-Layer Architecture Services +from .base_service import BaseService +from .project_management_service import ProjectManagementService +from .index_management_service import IndexManagementService +from .file_discovery_service import FileDiscoveryService +from .code_intelligence_service import CodeIntelligenceService +from .system_management_service import SystemManagementService +from .search_service import SearchService # Already follows clean architecture +from .settings_service import SettingsService + +# Simple Services +from .file_service import FileService # Simple file reading for resources +from .file_watcher_service import FileWatcherService # Low-level service, still needed + +__all__ = [ + # New Architecture + 'BaseService', + 'ProjectManagementService', + 'IndexManagementService', + 'FileDiscoveryService', + 'CodeIntelligenceService', + 'SystemManagementService', + 'SearchService', + 'SettingsService', + + # Simple Services + 'FileService', # Simple file reading for resources + 'FileWatcherService' # Keep as low-level service +] \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/base_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/base_service.py new file mode 100644 index 00000000..a29e6bfc --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/base_service.py @@ -0,0 +1,140 @@ +""" +Base service class providing common functionality for all services. + +This module defines the base service pattern that all domain services inherit from, +ensuring consistent behavior and shared functionality across the service layer. +""" + +from abc import ABC +from typing import Optional +from mcp.server.fastmcp import Context + +from ..utils import ContextHelper, ValidationHelper + + +class BaseService(ABC): + """ + Base class for all MCP services. + + This class provides common functionality that all services need: + - Context management through ContextHelper + - Common validation patterns + - Shared error checking methods + + All domain services should inherit from this class to ensure + consistent behavior and access to shared utilities. + """ + + def __init__(self, ctx: Context): + """ + Initialize the base service. + + Args: + ctx: The MCP Context object containing request and lifespan context + """ + self.ctx = ctx + self.helper = ContextHelper(ctx) + + def _validate_project_setup(self) -> Optional[str]: + """ + Validate that the project is properly set up. + + This method checks if the base path is set and valid, which is + required for most operations. + + Returns: + Error message if project is not set up properly, None if valid + """ + return self.helper.get_base_path_error() + + def _require_project_setup(self) -> None: + """ + Ensure project is set up, raising an exception if not. + + This is a convenience method for operations that absolutely + require a valid project setup. + + Raises: + ValueError: If project is not properly set up + """ + error = self._validate_project_setup() + if error: + raise ValueError(error) + + def _validate_file_path(self, file_path: str) -> Optional[str]: + """ + Validate a file path for security and accessibility. + + Args: + file_path: The file path to validate + + Returns: + Error message if validation fails, None if valid + """ + return ValidationHelper.validate_file_path(file_path, self.helper.base_path) + + def _require_valid_file_path(self, file_path: str) -> None: + """ + Ensure file path is valid, raising an exception if not. + + Args: + file_path: The file path to validate + + Raises: + ValueError: If file path is invalid + """ + error = self._validate_file_path(file_path) + if error: + raise ValueError(error) + + @property + def base_path(self) -> str: + """ + Convenient access to the base project path. + + Returns: + The base project path + """ + return self.helper.base_path + + @property + def settings(self): + """ + Convenient access to the project settings. + + Returns: + The ProjectSettings instance + """ + return self.helper.settings + + @property + def file_count(self) -> int: + """ + Convenient access to the current file count. + + Returns: + The number of indexed files + """ + return self.helper.file_count + + @property + def index_provider(self): + """ + Convenient access to the unified index provider. + + Returns: + The current IIndexProvider instance, or None if not available + """ + if self.helper.index_manager: + return self.helper.index_manager.get_provider() + return None + + @property + def index_manager(self): + """ + Convenient access to the index manager. + + Returns: + The index manager instance, or None if not available + """ + return self.helper.index_manager diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/code_intelligence_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/code_intelligence_service.py new file mode 100644 index 00000000..af0f1a25 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/code_intelligence_service.py @@ -0,0 +1,104 @@ +""" +Code Intelligence Service - Business logic for code analysis and understanding. + +This service handles the business logic for analyzing code files using the new +JSON-based indexing system optimized for LLM consumption. +""" + +import logging +import os +from typing import Dict, Any + +from .base_service import BaseService +from ..tools.filesystem import FileSystemTool +from ..indexing import get_index_manager + +logger = logging.getLogger(__name__) + + +class CodeIntelligenceService(BaseService): + """ + Business service for code analysis and intelligence using JSON indexing. + + This service provides comprehensive code analysis using the optimized + JSON-based indexing system for fast LLM-friendly responses. + """ + + def __init__(self, ctx): + super().__init__(ctx) + self._filesystem_tool = FileSystemTool() + + def analyze_file(self, file_path: str) -> Dict[str, Any]: + """ + Analyze a file and return comprehensive intelligence. + + This is the main business method that orchestrates the file analysis + workflow, choosing the best analysis strategy and providing rich + insights about the code. + + Args: + file_path: Path to the file to analyze (relative to project root) + + Returns: + Dictionary with comprehensive file analysis + + Raises: + ValueError: If file path is invalid or analysis fails + """ + # Business validation + self._validate_analysis_request(file_path) + + # Use the global index manager + index_manager = get_index_manager() + + # Debug logging + logger.info(f"Getting file summary for: {file_path}") + logger.info(f"Index manager state - Project path: {index_manager.project_path}") + logger.info(f"Index manager state - Has builder: {index_manager.index_builder is not None}") + if index_manager.index_builder: + logger.info(f"Index manager state - Has index: {index_manager.index_builder.in_memory_index is not None}") + + # Get file summary from JSON index + summary = index_manager.get_file_summary(file_path) + logger.info(f"Summary result: {summary is not None}") + + # If deep index isn't available yet, return a helpful hint instead of error + if not summary: + return { + "status": "needs_deep_index", + "message": "Deep index not available. Please run build_deep_index before calling get_file_summary.", + "file_path": file_path + } + + return summary + + def _validate_analysis_request(self, file_path: str) -> None: + """ + Validate the file analysis request according to business rules. + + Args: + file_path: File path to validate + + Raises: + ValueError: If validation fails + """ + # Business rule: Project must be set up OR auto-initialization must be possible + if self.base_path: + # Standard validation if project is set up in context + self._require_valid_file_path(file_path) + full_path = os.path.join(self.base_path, file_path) + if not os.path.exists(full_path): + raise ValueError(f"File does not exist: {file_path}") + else: + # Allow proceeding if auto-initialization might work + # The index manager will handle project discovery + logger.info("Project not set in context, relying on index auto-initialization") + + # Basic file path validation only + if not file_path or '..' in file_path: + raise ValueError(f"Invalid file path: {file_path}") + + + + + diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/file_discovery_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/file_discovery_service.py new file mode 100644 index 00000000..d7775112 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/file_discovery_service.py @@ -0,0 +1,78 @@ +""" +File Discovery Service - Business logic for intelligent file discovery. + +This service handles the business logic for finding files using the new +JSON-based indexing system optimized for LLM consumption. +""" + +from typing import Dict, Any, List, Optional +from dataclasses import dataclass + +from .base_service import BaseService +from ..indexing import get_shallow_index_manager + + +@dataclass +class FileDiscoveryResult: + """Business result for file discovery operations.""" + files: List[str] + total_count: int + pattern_used: str + search_strategy: str + metadata: Dict[str, Any] + + +class FileDiscoveryService(BaseService): + """ + Business service for intelligent file discovery using JSON indexing. + + This service provides fast file discovery using the optimized JSON + indexing system for efficient LLM-oriented responses. + """ + + def __init__(self, ctx): + super().__init__(ctx) + self._index_manager = get_shallow_index_manager() + + def find_files(self, pattern: str, max_results: Optional[int] = None) -> List[str]: + """ + Find files matching the given pattern using JSON indexing. + + Args: + pattern: Glob pattern to search for (e.g., "*.py", "test_*.js") + max_results: Maximum number of results to return (None for no limit) + + Returns: + List of file paths matching the pattern + + Raises: + ValueError: If pattern is invalid or project not set up + """ + # Business validation + self._validate_discovery_request(pattern) + + # Get files from JSON index + files = self._index_manager.find_files(pattern) + + # Apply max_results limit if specified + if max_results and len(files) > max_results: + files = files[:max_results] + + return files + + def _validate_discovery_request(self, pattern: str) -> None: + """ + Validate the file discovery request according to business rules. + + Args: + pattern: Pattern to validate + + Raises: + ValueError: If validation fails + """ + # Ensure project is set up + self._require_project_setup() + + # Validate pattern + if not pattern or not pattern.strip(): + raise ValueError("Search pattern cannot be empty") diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/file_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/file_service.py new file mode 100644 index 00000000..480d6f06 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/file_service.py @@ -0,0 +1,62 @@ +""" +File Service - Simple file reading service for MCP resources. + +This service provides simple file content reading functionality for MCP resources. +Complex file analysis has been moved to CodeIntelligenceService. + +Usage: +- get_file_content() - used by files://{file_path} resource +""" + +import os +from .base_service import BaseService + + +class FileService(BaseService): + """ + Simple service for file content reading. + + This service handles basic file reading operations for MCP resources. + Complex analysis functionality has been moved to CodeIntelligenceService. + """ + + def get_file_content(self, file_path: str) -> str: + """ + Get file content for MCP resource. + + Args: + file_path: Path to the file (relative to project root) + + Returns: + File content as string + + Raises: + ValueError: If project is not set up or path is invalid + FileNotFoundError: If file is not found or readable + """ + self._require_project_setup() + self._require_valid_file_path(file_path) + + # Build full path + full_path = os.path.join(self.base_path, file_path) + + try: + # Try UTF-8 first (most common) + with open(full_path, 'r', encoding='utf-8') as f: + return f.read() + except UnicodeDecodeError: + # Try other encodings if UTF-8 fails + encodings = ['utf-8-sig', 'latin-1', 'cp1252', 'iso-8859-1'] + for encoding in encodings: + try: + with open(full_path, 'r', encoding=encoding) as f: + return f.read() + except UnicodeDecodeError: + continue + + raise ValueError( + f"Could not decode file {file_path}. File may have " + f"unsupported encoding." + ) from None + except (FileNotFoundError, PermissionError, OSError) as e: + raise FileNotFoundError(f"Error reading file: {e}") from e diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/file_watcher_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/file_watcher_service.py new file mode 100644 index 00000000..c2ef64c6 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/file_watcher_service.py @@ -0,0 +1,418 @@ +""" +File Watcher Service for automatic index rebuilds. + +This module provides file system monitoring capabilities that automatically +trigger index rebuilds when relevant files are modified, created, or deleted. +It uses the watchdog library for cross-platform file system event monitoring. +""" +# pylint: disable=missing-function-docstring # Fallback stub methods don't need docstrings + +import logging +import os +import traceback +from threading import Timer +from typing import Optional, Callable, List +from pathlib import Path + +try: + from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler, FileSystemEvent + WATCHDOG_AVAILABLE = True +except ImportError: + # Fallback classes for when watchdog is not available + class Observer: + """Fallback Observer class when watchdog library is not available.""" + def __init__(self): + pass + def schedule(self, *args, **kwargs): + pass + def start(self): + pass + def stop(self): + pass + def join(self, *args, **kwargs): + pass + def is_alive(self): + return False + + class FileSystemEventHandler: + """Fallback FileSystemEventHandler class when watchdog library is not available.""" + def __init__(self): + pass + + class FileSystemEvent: + """Fallback FileSystemEvent class when watchdog library is not available.""" + def __init__(self): + self.is_directory = False + self.src_path = "" + self.event_type = "" + + WATCHDOG_AVAILABLE = False + +from .base_service import BaseService +from ..constants import SUPPORTED_EXTENSIONS + + +class FileWatcherService(BaseService): + """ + Service for monitoring file system changes and triggering index rebuilds. + + This service uses the watchdog library to monitor file system events and + automatically triggers background index rebuilds when relevant files change. + It includes intelligent debouncing to batch rapid changes and filtering + to only monitor relevant file types. + """ + MAX_RESTART_ATTEMPTS = 3 + + def __init__(self, ctx): + """ + Initialize the file watcher service. + + Args: + ctx: The MCP Context object + """ + super().__init__(ctx) + self.logger = logging.getLogger(__name__) + self.observer: Optional[Observer] = None + self.event_handler: Optional[DebounceEventHandler] = None + self.is_monitoring = False + self.restart_attempts = 0 + self.rebuild_callback: Optional[Callable] = None + + # Check if watchdog is available + if not WATCHDOG_AVAILABLE: + self.logger.warning("Watchdog library not available - file watcher disabled") + + def start_monitoring(self, rebuild_callback: Callable) -> bool: + """ + Start file system monitoring. + + Args: + rebuild_callback: Function to call when rebuild is needed + + Returns: + True if monitoring started successfully, False otherwise + """ + if not WATCHDOG_AVAILABLE: + self.logger.warning("Cannot start file watcher - watchdog library not available") + return False + + if self.is_monitoring: + self.logger.debug("File watcher already monitoring") + return True + + # Validate project setup + error = self._validate_project_setup() + if error: + self.logger.error("Cannot start file watcher: %s", error) + return False + + self.rebuild_callback = rebuild_callback + + # Get debounce seconds from config + config = self.settings.get_file_watcher_config() + debounce_seconds = config.get('debounce_seconds', 6.0) + + try: + self.observer = Observer() + self.event_handler = DebounceEventHandler( + debounce_seconds=debounce_seconds, + rebuild_callback=self.rebuild_callback, + base_path=Path(self.base_path), + logger=self.logger + ) + + # Log detailed Observer setup + watch_path = str(self.base_path) + self.logger.debug("Scheduling Observer for path: %s", watch_path) + + self.observer.schedule( + self.event_handler, + watch_path, + recursive=True + ) + + # Log Observer start + self.logger.debug("Starting Observer...") + self.observer.start() + self.is_monitoring = True + self.restart_attempts = 0 + + # Log Observer thread info + if hasattr(self.observer, '_thread'): + self.logger.debug("Observer thread: %s", self.observer._thread) + + # Verify observer is actually running + if self.observer.is_alive(): + self.logger.info( + "File watcher started successfully", + extra={ + "debounce_seconds": debounce_seconds, + "monitored_path": str(self.base_path), + "supported_extensions": len(SUPPORTED_EXTENSIONS) + } + ) + + # Add diagnostic test - create a test event to verify Observer works + self.logger.debug("Observer thread is alive: %s", self.observer.is_alive()) + self.logger.debug("Monitored path exists: %s", os.path.exists(str(self.base_path))) + self.logger.debug("Event handler is set: %s", self.event_handler is not None) + + # Log current directory for comparison + current_dir = os.getcwd() + self.logger.debug("Current working directory: %s", current_dir) + self.logger.debug("Are paths same: %s", os.path.normpath(current_dir) == os.path.normpath(str(self.base_path))) + + return True + else: + self.logger.error("File watcher failed to start - Observer not alive") + return False + + except Exception as e: + self.logger.warning("Failed to start file watcher: %s", e) + self.logger.info("Falling back to reactive index refresh") + return False + + def stop_monitoring(self) -> None: + """ + Stop file system monitoring and cleanup all resources. + + This method ensures complete cleanup of: + - Observer thread + - Event handler + - Debounce timers + - Monitoring state + """ + if not self.observer and not self.is_monitoring: + # Already stopped or never started + return + + self.logger.info("Stopping file watcher monitoring...") + + try: + # Step 1: Stop the observer first + if self.observer: + self.logger.debug("Stopping observer...") + self.observer.stop() + + # Step 2: Cancel any active debounce timer + if self.event_handler and self.event_handler.debounce_timer: + self.logger.debug("Cancelling debounce timer...") + self.event_handler.debounce_timer.cancel() + + # Step 3: Wait for observer thread to finish (with timeout) + self.logger.debug("Waiting for observer thread to finish...") + self.observer.join(timeout=5.0) + + # Step 4: Check if thread actually finished + if self.observer.is_alive(): + self.logger.warning("Observer thread did not stop within timeout") + else: + self.logger.debug("Observer thread stopped successfully") + + # Step 5: Clear all references + self.observer = None + self.event_handler = None + self.rebuild_callback = None + self.is_monitoring = False + + self.logger.info("File watcher stopped and cleaned up successfully") + + except Exception as e: + self.logger.error("Error stopping file watcher: %s", e) + + # Force cleanup even if there were errors + self.observer = None + self.event_handler = None + self.rebuild_callback = None + self.is_monitoring = False + + def is_active(self) -> bool: + """ + Check if file watcher is actively monitoring. + + Returns: + True if actively monitoring, False otherwise + """ + return (self.is_monitoring and + self.observer and + self.observer.is_alive()) + + def restart_observer(self) -> bool: + """ + Attempt to restart the file system observer. + + Returns: + True if restart successful, False otherwise + """ + if self.restart_attempts >= self.MAX_RESTART_ATTEMPTS: + self.logger.error("Max restart attempts reached, file watcher disabled") + return False + + self.logger.info("Attempting to restart file watcher (attempt %d)", + self.restart_attempts + 1) + self.restart_attempts += 1 + + # Stop current observer if running + if self.observer: + try: + self.observer.stop() + self.observer.join(timeout=2.0) + except Exception as e: + self.logger.warning("Error stopping observer during restart: %s", e) + + # Start new observer + try: + self.observer = Observer() + self.observer.schedule( + self.event_handler, + str(self.base_path), + recursive=True + ) + self.observer.start() + self.is_monitoring = True + + self.logger.info("File watcher restarted successfully") + return True + + except Exception as e: + self.logger.error("Failed to restart file watcher: %s", e) + return False + + def get_status(self) -> dict: + """ + Get current file watcher status information. + + Returns: + Dictionary containing status information + """ + # Get current debounce seconds from config + config = self.settings.get_file_watcher_config() + debounce_seconds = config.get('debounce_seconds', 6.0) + + return { + "available": WATCHDOG_AVAILABLE, + "active": self.is_active(), + "monitoring": self.is_monitoring, + "restart_attempts": self.restart_attempts, + "debounce_seconds": debounce_seconds, + "base_path": self.base_path if self.base_path else None, + "observer_alive": self.observer.is_alive() if self.observer else False + } + + +class DebounceEventHandler(FileSystemEventHandler): + """ + File system event handler with debouncing capability. + + This handler filters file system events to only relevant files and + implements a debounce mechanism to batch rapid changes into single + rebuild operations. + """ + + def __init__(self, debounce_seconds: float, rebuild_callback: Callable, + base_path: Path, logger: logging.Logger, additional_excludes: Optional[List[str]] = None): + """ + Initialize the debounce event handler. + + Args: + debounce_seconds: Number of seconds to wait before triggering rebuild + rebuild_callback: Function to call when rebuild is needed + base_path: Base project path for filtering + logger: Logger instance for debug messages + additional_excludes: Additional patterns to exclude + """ + from ..utils import FileFilter + + super().__init__() + self.debounce_seconds = debounce_seconds + self.rebuild_callback = rebuild_callback + self.base_path = base_path + self.debounce_timer: Optional[Timer] = None + self.logger = logger + + # Use centralized file filtering + self.file_filter = FileFilter(additional_excludes) + + def on_any_event(self, event: FileSystemEvent) -> None: + """ + Handle any file system event. + + Args: + event: The file system event + """ + # Check if event should be processed + should_process = self.should_process_event(event) + + if should_process: + self.logger.info("File changed: %s - %s", event.event_type, event.src_path) + self.reset_debounce_timer() + else: + # Only log at debug level for filtered events + self.logger.debug("Filtered: %s - %s", event.event_type, event.src_path) + + def should_process_event(self, event: FileSystemEvent) -> bool: + """ + Determine if event should trigger index rebuild using centralized filtering. + + Args: + event: The file system event to evaluate + + Returns: + True if event should trigger rebuild, False otherwise + """ + # Skip directory events + if event.is_directory: + self.logger.debug("Skipping directory event: %s", event.src_path) + return False + + # Select path to check: dest_path for moves, src_path for others + if event.event_type == 'moved': + if not hasattr(event, 'dest_path'): + return False + target_path = event.dest_path + else: + target_path = event.src_path + + # Use centralized filtering logic + try: + path = Path(target_path) + should_process = self.file_filter.should_process_path(path, self.base_path) + + # Skip temporary files using centralized logic + if not should_process or self.file_filter.is_temporary_file(path): + return False + + return True + except Exception: + return False + + + + + + + def reset_debounce_timer(self) -> None: + """Reset the debounce timer, canceling any existing timer.""" + if self.debounce_timer: + self.debounce_timer.cancel() + + self.debounce_timer = Timer( + self.debounce_seconds, + self.trigger_rebuild + ) + self.debounce_timer.start() + + def trigger_rebuild(self) -> None: + """Trigger index rebuild after debounce period.""" + self.logger.info("File changes detected, triggering rebuild") + + if self.rebuild_callback: + try: + result = self.rebuild_callback() + except Exception as e: + self.logger.error("Rebuild callback failed: %s", e) + traceback_msg = traceback.format_exc() + self.logger.error("Traceback: %s", traceback_msg) + else: + self.logger.warning("No rebuild callback configured") diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/index_management_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/index_management_service.py new file mode 100644 index 00000000..f56c7608 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/index_management_service.py @@ -0,0 +1,198 @@ +""" +Index Management Service - Business logic for index lifecycle management. + +This service handles the business logic for index rebuilding, status monitoring, +and index-related operations using the new JSON-based indexing system. +""" +import time +import logging +import os +import json + +from typing import Dict, Any +from dataclasses import dataclass + +logger = logging.getLogger(__name__) + +from .base_service import BaseService +from ..indexing import get_index_manager, get_shallow_index_manager, DeepIndexManager + + +@dataclass +class IndexRebuildResult: + """Business result for index rebuild operations.""" + file_count: int + rebuild_time: float + status: str + message: str + + +class IndexManagementService(BaseService): + """ + Business service for index lifecycle management. + + This service orchestrates index management workflows using the new + JSON-based indexing system for optimal LLM performance. + """ + + def __init__(self, ctx): + super().__init__(ctx) + # Deep manager (symbols/files, legacy JSON index manager) + self._index_manager = get_index_manager() + # Shallow manager (file-list only) for default workflows + self._shallow_manager = get_shallow_index_manager() + # Optional wrapper for explicit deep builds + self._deep_wrapper = DeepIndexManager() + + def rebuild_index(self) -> str: + """ + Rebuild the project index (DEFAULT: shallow file list). + + For deep/symbol rebuilds, use build_deep_index() tool instead. + + Returns: + Success message with rebuild information + + Raises: + ValueError: If project not set up or rebuild fails + """ + # Business validation + self._validate_rebuild_request() + + # Shallow rebuild only (fast path) + if not self._shallow_manager.set_project_path(self.base_path): + raise RuntimeError("Failed to set project path (shallow) in index manager") + if not self._shallow_manager.build_index(): + raise RuntimeError("Failed to rebuild shallow index") + + try: + count = len(self._shallow_manager.get_file_list()) + except Exception: + count = 0 + return f"Shallow index re-built with {count} files." + + def get_rebuild_status(self) -> Dict[str, Any]: + """ + Get current index rebuild status information. + + Returns: + Dictionary with rebuild status and metadata + """ + # Check if project is set up + if not self.base_path: + return { + 'status': 'not_initialized', + 'message': 'Project not initialized', + 'is_rebuilding': False + } + + # Get index stats from the new JSON system + stats = self._index_manager.get_index_stats() + + return { + 'status': 'ready' if stats.get('status') == 'loaded' else 'needs_rebuild', + 'index_available': stats.get('status') == 'loaded', + 'is_rebuilding': False, + 'project_path': self.base_path, + 'file_count': stats.get('indexed_files', 0), + 'total_symbols': stats.get('total_symbols', 0), + 'symbol_types': stats.get('symbol_types', {}), + 'languages': stats.get('languages', []) + } + + def _validate_rebuild_request(self) -> None: + """ + Validate the index rebuild request according to business rules. + + Raises: + ValueError: If validation fails + """ + # Business rule: Project must be set up + self._require_project_setup() + + def _execute_rebuild_workflow(self) -> IndexRebuildResult: + """ + Execute the core index rebuild business workflow. + + Returns: + IndexRebuildResult with rebuild data + """ + start_time = time.time() + + # Set project path in index manager + if not self._index_manager.set_project_path(self.base_path): + raise RuntimeError("Failed to set project path in index manager") + + # Rebuild the index + if not self._index_manager.refresh_index(): + raise RuntimeError("Failed to rebuild index") + + # Get stats for result + stats = self._index_manager.get_index_stats() + file_count = stats.get('indexed_files', 0) + + rebuild_time = time.time() - start_time + + return IndexRebuildResult( + file_count=file_count, + rebuild_time=rebuild_time, + status='success', + message=f"Index rebuilt successfully with {file_count} files" + ) + + + def _format_rebuild_result(self, result: IndexRebuildResult) -> str: + """ + Format the rebuild result according to business requirements. + + Args: + result: Rebuild result data + + Returns: + Formatted result string for MCP response + """ + return f"Project re-indexed. Found {result.file_count} files." + + def build_shallow_index(self) -> str: + """ + Build and persist the shallow index (file list only). + + Returns: + Success message including file count if available. + + Raises: + ValueError/RuntimeError on validation or build failure + """ + # Ensure project is set up + self._require_project_setup() + + # Initialize manager with current base path + if not self._shallow_manager.set_project_path(self.base_path): + raise RuntimeError("Failed to set project path in index manager") + + # Build shallow index + if not self._shallow_manager.build_index(): + raise RuntimeError("Failed to build shallow index") + + # Try to report count + count = 0 + try: + shallow_path = getattr(self._shallow_manager, 'index_path', None) + if shallow_path and os.path.exists(shallow_path): + with open(shallow_path, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, list): + count = len(data) + except Exception as e: # noqa: BLE001 - safe fallback to zero + logger.debug(f"Unable to read shallow index count: {e}") + + return f"Shallow index built{f' with {count} files' if count else ''}." + + def rebuild_deep_index(self) -> str: + """Rebuild the deep index using the original workflow.""" + # Business validation + self._validate_rebuild_request() + + # Deep rebuild via existing workflow + result = self._execute_rebuild_workflow() + return self._format_rebuild_result(result) diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/project_management_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/project_management_service.py new file mode 100644 index 00000000..ef9c8bff --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/project_management_service.py @@ -0,0 +1,375 @@ +""" +Project Management Service - Business logic for project lifecycle management. + +This service handles the business logic for project initialization, configuration, +and lifecycle management using the new JSON-based indexing system. +""" +import logging +from typing import Dict, Any +from dataclasses import dataclass +from contextlib import contextmanager + +from .base_service import BaseService +from ..utils.response_formatter import ResponseFormatter +from ..constants import SUPPORTED_EXTENSIONS +from ..indexing import get_index_manager, get_shallow_index_manager + +logger = logging.getLogger(__name__) + + +@dataclass +class ProjectInitializationResult: + """Business result for project initialization operations.""" + project_path: str + file_count: int + index_source: str # 'loaded_existing' or 'built_new' + search_capabilities: str + monitoring_status: str + message: str + + +class ProjectManagementService(BaseService): + """ + Business service for project lifecycle management. + + This service orchestrates project initialization workflows by composing + technical tools to achieve business goals like setting up projects, + managing configurations, and coordinating system components. + """ + + def __init__(self, ctx): + super().__init__(ctx) + # Deep index manager (legacy full index) + self._index_manager = get_index_manager() + # Shallow index manager (default for initialization) + self._shallow_manager = get_shallow_index_manager() + from ..tools.config import ProjectConfigTool + self._config_tool = ProjectConfigTool() + # Import FileWatcherTool locally to avoid circular import + from ..tools.monitoring import FileWatcherTool + self._watcher_tool = FileWatcherTool(ctx) + + + @contextmanager + def _noop_operation(self, *_args, **_kwargs): + yield + + def initialize_project(self, path: str) -> str: + """ + Initialize a project with comprehensive business logic. + + This is the main business method that orchestrates the project + initialization workflow, handling validation, cleanup, setup, + and coordination of all project components. + + Args: + path: Project directory path to initialize + + Returns: + Success message with project information + + Raises: + ValueError: If path is invalid or initialization fails + """ + # Business validation + self._validate_initialization_request(path) + + # Business workflow: Execute initialization + result = self._execute_initialization_workflow(path) + + # Business result formatting + return self._format_initialization_result(result) + + def _validate_initialization_request(self, path: str) -> None: + """ + Validate the project initialization request according to business rules. + + Args: + path: Project path to validate + + Raises: + ValueError: If validation fails + """ + # Business rule: Path must be valid + error = self._config_tool.validate_project_path(path) + if error: + raise ValueError(error) + + def _execute_initialization_workflow(self, path: str) -> ProjectInitializationResult: + """ + Execute the core project initialization business workflow. + + Args: + path: Project path to initialize + + Returns: + ProjectInitializationResult with initialization data + """ + # Business step 1: Initialize config tool + self._config_tool.initialize_settings(path) + + # Normalize path for consistent processing + normalized_path = self._config_tool.normalize_project_path(path) + + # Business step 2: Cleanup existing project state + self._cleanup_existing_project() + + # Business step 3: Initialize shallow index by default (fast path) + index_result = self._initialize_shallow_index_manager(normalized_path) + + # Business step 3.1: Store index manager in context for other services + self.helper.update_index_manager(self._index_manager) + + # Business step 4: Setup file monitoring + monitoring_result = self._setup_file_monitoring(normalized_path) + + # Business step 4: Update system state + self._update_project_state(normalized_path, index_result['file_count']) + + # Business step 6: Get search capabilities info + search_info = self._get_search_capabilities_info() + + return ProjectInitializationResult( + project_path=normalized_path, + file_count=index_result['file_count'], + index_source=index_result['source'], + search_capabilities=search_info, + monitoring_status=monitoring_result, + message=f"Project initialized: {normalized_path}" + ) + + def _cleanup_existing_project(self) -> None: + """Business logic to cleanup existing project state.""" + with self._noop_operation(): + # Stop existing file monitoring + self._watcher_tool.stop_existing_watcher() + + # Clear existing index cache + self.helper.clear_index_cache() + + # Clear any existing index state + pass + + def _initialize_shallow_index_manager(self, project_path: str) -> Dict[str, Any]: + """ + Business logic to initialize the shallow index manager by default. + + Args: + project_path: Project path + + Returns: + Dictionary with initialization results + """ + # Set project path in shallow manager + if not self._shallow_manager.set_project_path(project_path): + raise RuntimeError(f"Failed to set project path (shallow): {project_path}") + + # Update context + self.helper.update_base_path(project_path) + + # Try to load existing shallow index or build new one + if self._shallow_manager.load_index(): + source = "loaded_existing" + else: + if not self._shallow_manager.build_index(): + raise RuntimeError("Failed to build shallow index") + source = "built_new" + + # Determine file count from shallow list + try: + files = self._shallow_manager.get_file_list() + file_count = len(files) + except Exception: # noqa: BLE001 - safe fallback + file_count = 0 + + return { + 'file_count': file_count, + 'source': source, + 'total_symbols': 0, + 'languages': [] + } + + + def _is_valid_existing_index(self, index_data: Dict[str, Any]) -> bool: + """ + Business rule to determine if existing index is valid and usable. + + Args: + index_data: Index data to validate + + Returns: + True if index is valid and usable, False otherwise + """ + if not index_data or not isinstance(index_data, dict): + return False + + # Business rule: Must have new format metadata + if 'index_metadata' not in index_data: + return False + + # Business rule: Must be compatible version + version = index_data.get('index_metadata', {}).get('version', '') + return version >= '3.0' + + def _load_existing_index(self, index_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Business logic to load and use existing index. + + Args: + index_data: Existing index data + + Returns: + Dictionary with loading results + """ + + + # Note: Legacy index loading is now handled by UnifiedIndexManager + # This method is kept for backward compatibility but functionality moved + + # Extract file count from metadata + file_count = index_data.get('project_metadata', {}).get('total_files', 0) + + + + return { + 'file_count': file_count, + 'source': 'loaded_existing' + } + + + def _setup_file_monitoring(self, project_path: str) -> str: + """ + Business logic to setup file monitoring for the project. + + Args: + project_path: Project path to monitor + + Returns: + String describing monitoring setup result + """ + + + try: + # Create rebuild callback that uses the JSON index manager + def rebuild_callback(): + logger.info("File watcher triggered rebuild callback") + try: + logger.debug(f"Starting shallow index rebuild for: {project_path}") + # Business logic: File changed, rebuild using SHALLOW index manager + try: + if not self._shallow_manager.set_project_path(project_path): + logger.warning("Shallow manager set_project_path failed") + return False + if self._shallow_manager.build_index(): + files = self._shallow_manager.get_file_list() + logger.info(f"File watcher shallow rebuild completed successfully - files {len(files)}") + return True + else: + logger.warning("File watcher shallow rebuild failed") + return False + except Exception as e: + import traceback + logger.error(f"File watcher shallow rebuild failed: {e}") + logger.error(f"Traceback: {traceback.format_exc()}") + return False + except Exception as e: + import traceback + logger.error(f"File watcher rebuild failed: {e}") + logger.error(f"Traceback: {traceback.format_exc()}") + return False + + # Start monitoring using watcher tool + success = self._watcher_tool.start_monitoring(project_path, rebuild_callback) + + if success: + # Store watcher in context for later access + self._watcher_tool.store_in_context() + # No logging + return "monitoring_active" + else: + self._watcher_tool.record_error("Failed to start file monitoring") + return "monitoring_failed" + + except Exception as e: + error_msg = f"File monitoring setup failed: {e}" + self._watcher_tool.record_error(error_msg) + return "monitoring_error" + + def _update_project_state(self, project_path: str, file_count: int) -> None: + """Business logic to update system state after project initialization.""" + + + # Update context with file count + self.helper.update_file_count(file_count) + + # No logging + + def _get_search_capabilities_info(self) -> str: + """Business logic to get search capabilities information.""" + search_info = self._config_tool.get_search_tool_info() + + if search_info['available']: + return f"Advanced search enabled ({search_info['name']})" + else: + return "Basic search available" + + def _format_initialization_result(self, result: ProjectInitializationResult) -> str: + """ + Format the initialization result according to business requirements. + + Args: + result: Initialization result data + + Returns: + Formatted result string for MCP response + """ + if result.index_source == 'unified_manager': + message = (f"Project path set to: {result.project_path}. " + f"Initialized unified index with {result.file_count} files. " + f"{result.search_capabilities}.") + elif result.index_source == 'failed': + message = (f"Project path set to: {result.project_path}. " + f"Index initialization failed. Some features may be limited. " + f"{result.search_capabilities}.") + else: + message = (f"Project path set to: {result.project_path}. " + f"Indexed {result.file_count} files. " + f"{result.search_capabilities}.") + + if result.monitoring_status != "monitoring_active": + message += " (File monitoring unavailable - use manual refresh)" + + return message + + def get_project_config(self) -> str: + """ + Get the current project configuration for MCP resource. + + Returns: + JSON formatted configuration string + """ + + # Check if project is configured + if not self.helper.base_path: + config_data = { + "status": "not_configured", + "message": ("Project path not set. Please use set_project_path " + "to set a project directory first."), + "supported_extensions": SUPPORTED_EXTENSIONS + } + return ResponseFormatter.config_response(config_data) + + # Get settings stats + settings_stats = self.helper.settings.get_stats() if self.helper.settings else {} + + config_data = { + "base_path": self.helper.base_path, + "supported_extensions": SUPPORTED_EXTENSIONS, + "file_count": self.helper.file_count, + "settings_directory": self.helper.settings.settings_path if self.helper.settings else "", + "settings_stats": settings_stats + } + + return ResponseFormatter.config_response(config_data) + + # Removed: get_project_structure; the project structure resource is deprecated diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/search_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/search_service.py new file mode 100644 index 00000000..151843e6 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/search_service.py @@ -0,0 +1,269 @@ +""" +Search service for the Code Index MCP server. + +This service handles code search operations, search tool management, +and search strategy selection. +""" + +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +from .base_service import BaseService +from ..utils import FileFilter, ResponseFormatter, ValidationHelper +from ..search.base import is_safe_regex_pattern + + +class SearchService(BaseService): + """Service for managing code search operations.""" + + def __init__(self, ctx): + super().__init__(ctx) + self.file_filter = self._create_file_filter() + + def search_code( # pylint: disable=too-many-arguments, too-many-locals + self, + pattern: str, + case_sensitive: bool = True, + context_lines: int = 0, + file_pattern: Optional[str] = None, + fuzzy: bool = False, + regex: Optional[bool] = None, + start_index: int = 0, + max_results: Optional[int] = 10 + ) -> Dict[str, Any]: + """Search for code patterns in the project.""" + self._require_project_setup() + + if regex is None: + regex = is_safe_regex_pattern(pattern) + + error = ValidationHelper.validate_search_pattern(pattern, regex) + if error: + raise ValueError(error) + + if file_pattern: + error = ValidationHelper.validate_glob_pattern(file_pattern) + if error: + raise ValueError(f"Invalid file pattern: {error}") + + pagination_error = ValidationHelper.validate_pagination(start_index, max_results) + if pagination_error: + raise ValueError(pagination_error) + + if not self.settings: + raise ValueError("Settings not available") + + strategy = self.settings.get_preferred_search_tool() + if not strategy: + raise ValueError("No search strategies available") + + self._configure_strategy(strategy) + + try: + results = strategy.search( + pattern=pattern, + base_path=self.base_path, + case_sensitive=case_sensitive, + context_lines=context_lines, + file_pattern=file_pattern, + fuzzy=fuzzy, + regex=regex + ) + filtered = self._filter_results(results) + formatted_results, pagination = self._paginate_results( + filtered, + start_index=start_index, + max_results=max_results + ) + return ResponseFormatter.search_results_response( + formatted_results, + pagination + ) + except Exception as exc: + raise ValueError(f"Search failed using '{strategy.name}': {exc}") from exc + + def refresh_search_tools(self) -> str: + """Refresh the available search tools.""" + if not self.settings: + raise ValueError("Settings not available") + + self.settings.refresh_available_strategies() + config = self.settings.get_search_tools_config() + + available = config['available_tools'] + preferred = config['preferred_tool'] + return f"Search tools refreshed. Available: {available}. Preferred: {preferred}." + + def get_search_capabilities(self) -> Dict[str, Any]: + """Get information about search capabilities and available tools.""" + if not self.settings: + return {"error": "Settings not available"} + + config = self.settings.get_search_tools_config() + + capabilities = { + "available_tools": config.get('available_tools', []), + "preferred_tool": config.get('preferred_tool', 'basic'), + "supports_regex": True, + "supports_fuzzy": True, + "supports_case_sensitivity": True, + "supports_context_lines": True, + "supports_file_patterns": True + } + + return capabilities + + def _configure_strategy(self, strategy) -> None: + """Apply shared exclusion configuration to the strategy if supported.""" + configure = getattr(strategy, 'configure_excludes', None) + if not configure: + return + + try: + configure(self.file_filter) + except Exception: # pragma: no cover - defensive fallback + pass + + def _create_file_filter(self) -> FileFilter: + """Build a shared file filter drawing from project settings.""" + additional_dirs: List[str] = [] + additional_file_patterns: List[str] = [] + + settings = self.settings + if settings: + try: + config = settings.get_file_watcher_config() + except Exception: # pragma: no cover - fallback if config fails + config = {} + + for key in ('exclude_patterns', 'additional_exclude_patterns'): + patterns = config.get(key) or [] + for pattern in patterns: + if not isinstance(pattern, str): + continue + normalized = pattern.strip() + if not normalized: + continue + additional_dirs.append(normalized) + additional_file_patterns.append(normalized) + + file_filter = FileFilter(additional_dirs or None) + + if additional_file_patterns: + file_filter.exclude_files.update(additional_file_patterns) + + return file_filter + + def _filter_results(self, results: Dict[str, Any]) -> Dict[str, Any]: + """Filter out matches that reside under excluded paths.""" + if not isinstance(results, dict) or not results: + return results + + if 'error' in results or not self.file_filter or not self.base_path: + return results + + base_path = Path(self.base_path) + filtered: Dict[str, Any] = {} + + for rel_path, matches in results.items(): + if not isinstance(rel_path, str): + continue + + normalized = Path(rel_path.replace('\\', '/')) + try: + absolute = (base_path / normalized).resolve() + except Exception: # pragma: no cover - invalid path safety + continue + + try: + if self.file_filter.should_process_path(absolute, base_path): + filtered[rel_path] = matches + except Exception: # pragma: no cover - defensive fallback + continue + + return filtered + + def _paginate_results( + self, + results: Dict[str, Any], + start_index: int, + max_results: Optional[int] + ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + """Apply pagination to search results and format them for responses.""" + total_matches = 0 + for matches in results.values(): + if isinstance(matches, (list, tuple)): + total_matches += len(matches) + + effective_start = min(max(start_index, 0), total_matches) + + if total_matches == 0 or effective_start >= total_matches: + pagination = self._build_pagination_metadata( + total_matches=total_matches, + returned=0, + start_index=effective_start, + max_results=max_results + ) + return [], pagination + + collected: List[Dict[str, Any]] = [] + current_index = 0 + + sorted_items = sorted( + ( + (path, matches) + for path, matches in results.items() + if isinstance(path, str) and isinstance(matches, (list, tuple)) + ), + key=lambda item: item[0] + ) + + for path, matches in sorted_items: + sorted_matches = sorted( + (match for match in matches if isinstance(match, (list, tuple)) and len(match) >= 2), + key=lambda pair: pair[0] + ) + + for line_number, content, *_ in sorted_matches: + if current_index >= effective_start: + if max_results is None or len(collected) < max_results: + collected.append({ + "file": path, + "line": line_number, + "text": content + }) + else: + break + current_index += 1 + if max_results is not None and len(collected) >= max_results: + break + + pagination = self._build_pagination_metadata( + total_matches=total_matches, + returned=len(collected), + start_index=effective_start, + max_results=max_results + ) + return collected, pagination + + @staticmethod + def _build_pagination_metadata( + total_matches: int, + returned: int, + start_index: int, + max_results: Optional[int] + ) -> Dict[str, Any]: + """Construct pagination metadata for search responses.""" + end_index = start_index + returned + metadata: Dict[str, Any] = { + "total_matches": total_matches, + "returned": returned, + "start_index": start_index, + "has_more": end_index < total_matches + } + + if max_results is not None: + metadata["max_results"] = max_results + + metadata["end_index"] = end_index + return metadata diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/settings_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/settings_service.py new file mode 100644 index 00000000..bd641c4f --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/settings_service.py @@ -0,0 +1,191 @@ +""" +Settings management service for the Code Index MCP server. + +This service handles settings information, statistics, +temporary directory management, and settings cleanup operations. +""" + +import os +import tempfile +from typing import Dict, Any + +from .base_service import BaseService +from ..utils import ResponseFormatter +from ..constants import SETTINGS_DIR +from ..project_settings import ProjectSettings +from ..indexing import get_index_manager + + +def manage_temp_directory(action: str) -> Dict[str, Any]: + """ + Manage temporary directory operations. + + This is a standalone function that doesn't require project context. + Handles the logic for create_temp_directory and check_temp_directory MCP tools. + + Args: + action: The action to perform ('create' or 'check') + + Returns: + Dictionary with directory information and operation results + + Raises: + ValueError: If action is invalid or operation fails + """ + if action not in ['create', 'check']: + raise ValueError(f"Invalid action: {action}. Must be 'create' or 'check'") + + # Try to get the actual temp directory from index manager, fallback to default + try: + index_manager = get_index_manager() + temp_dir = index_manager.temp_dir if index_manager.temp_dir else os.path.join(tempfile.gettempdir(), SETTINGS_DIR) + except: + temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR) + + if action == 'create': + existed_before = os.path.exists(temp_dir) + + try: + # Use ProjectSettings to handle directory creation consistently + ProjectSettings("", skip_load=True) + + result = ResponseFormatter.directory_info_response( + temp_directory=temp_dir, + exists=os.path.exists(temp_dir), + is_directory=os.path.isdir(temp_dir) + ) + result["existed_before"] = existed_before + result["created"] = not existed_before + + return result + + except (OSError, IOError, ValueError) as e: + return ResponseFormatter.directory_info_response( + temp_directory=temp_dir, + exists=False, + error=str(e) + ) + + else: # action == 'check' + result = ResponseFormatter.directory_info_response( + temp_directory=temp_dir, + exists=os.path.exists(temp_dir), + is_directory=os.path.isdir(temp_dir) if os.path.exists(temp_dir) else False + ) + result["temp_root"] = tempfile.gettempdir() + + # If the directory exists, list its contents + if result["exists"] and result["is_directory"]: + try: + contents = os.listdir(temp_dir) + result["contents"] = contents + result["subdirectories"] = [] + + # Check each subdirectory + for item in contents: + item_path = os.path.join(temp_dir, item) + if os.path.isdir(item_path): + subdir_info = { + "name": item, + "path": item_path, + "contents": os.listdir(item_path) if os.path.exists(item_path) else [] + } + result["subdirectories"].append(subdir_info) + + except (OSError, PermissionError) as e: + result["error"] = str(e) + + return result + + + + + +class SettingsService(BaseService): + """ + Service for managing settings and directory operations. + + This service handles: + - Settings information and statistics + - Temporary directory management + - Settings cleanup operations + - Configuration data access + """ + + + + def get_settings_info(self) -> Dict[str, Any]: + """ + Get comprehensive settings information. + + Handles the logic for get_settings_info MCP tool. + + Returns: + Dictionary with settings directory, config, stats, and status information + """ + temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR) + + # Get the actual index directory from the index manager + index_manager = get_index_manager() + actual_temp_dir = index_manager.temp_dir if index_manager.temp_dir else temp_dir + + # Check if base_path is set + if not self.base_path: + return ResponseFormatter.settings_info_response( + settings_directory="", + temp_directory=actual_temp_dir, + temp_directory_exists=os.path.exists(actual_temp_dir), + config={}, + stats={}, + exists=False, + status="not_configured", + message="Project path not set. Please use set_project_path to set a " + "project directory first." + ) + + # Get config and stats + config = self.settings.load_config() if self.settings else {} + stats = self.settings.get_stats() if self.settings else {} + settings_directory = actual_temp_dir + exists = os.path.exists(settings_directory) if settings_directory else False + + return ResponseFormatter.settings_info_response( + settings_directory=settings_directory, + temp_directory=actual_temp_dir, + temp_directory_exists=os.path.exists(actual_temp_dir), + config=config, + stats=stats, + exists=exists + ) + + + + def clear_all_settings(self) -> str: + """ + Clear all settings and cached data. + + Handles the logic for clear_settings MCP tool. + + Returns: + Success message confirming settings were cleared + """ + if self.settings: + self.settings.clear() + + return "Project settings, index, and cache have been cleared." + + def get_settings_stats(self) -> str: + """ + Get settings statistics as JSON string. + + Handles the logic for settings://stats MCP resource. + + Returns: + JSON formatted settings statistics + """ + if not self.settings: + stats_data = {"error": "Settings not available"} + else: + stats_data = self.settings.get_stats() + + return ResponseFormatter.stats_response(stats_data) diff --git a/reference/code-index-mcp-master/src/code_index_mcp/services/system_management_service.py b/reference/code-index-mcp-master/src/code_index_mcp/services/system_management_service.py new file mode 100644 index 00000000..8cb420d4 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/services/system_management_service.py @@ -0,0 +1,407 @@ +""" +System Management Service - Business logic for system configuration and monitoring. + +This service handles the business logic for system management operations including +file watcher status, configuration management, and system health monitoring. +It composes technical tools to achieve business goals. +""" + +from typing import Dict, Any, Optional +from dataclasses import dataclass +from .index_management_service import IndexManagementService +from .base_service import BaseService +# FileWatcherTool will be imported locally to avoid circular import +from ..tools.config import ProjectConfigTool, SettingsTool + + +@dataclass +class FileWatcherStatus: + """Business result for file watcher status operations.""" + available: bool + active: bool + status: str + message: Optional[str] + error_info: Optional[Dict[str, Any]] + configuration: Dict[str, Any] + rebuild_status: Dict[str, Any] + recommendations: list[str] + + +class SystemManagementService(BaseService): + """ + Business service for system configuration and monitoring. + + This service orchestrates system management workflows by composing + technical tools to achieve business goals like monitoring file watchers, + managing configurations, and providing system health insights. + """ + + def __init__(self, ctx): + super().__init__(ctx) + # Import FileWatcherTool locally to avoid circular import + from ..tools.monitoring import FileWatcherTool + self._watcher_tool = FileWatcherTool(ctx) + self._config_tool = ProjectConfigTool() + self._settings_tool = SettingsTool() + + def get_file_watcher_status(self) -> Dict[str, Any]: + """ + Get comprehensive file watcher status with business intelligence. + + This is the main business method that orchestrates the file watcher + status workflow, analyzing system state, providing recommendations, + and formatting comprehensive status information. + + Returns: + Dictionary with comprehensive file watcher status + """ + # Business workflow: Analyze system state + status_result = self._analyze_file_watcher_state() + + # Business result formatting + return self._format_status_result(status_result) + + def configure_file_watcher(self, enabled: Optional[bool] = None, + debounce_seconds: Optional[float] = None, + additional_exclude_patterns: Optional[list] = None) -> str: + """ + Configure file watcher settings with business validation. + + Args: + enabled: Whether to enable file watcher + debounce_seconds: Debounce time in seconds + additional_exclude_patterns: Additional patterns to exclude + + Returns: + Success message with configuration details + + Raises: + ValueError: If configuration is invalid + """ + # Business validation + self._validate_configuration_request(enabled, debounce_seconds, additional_exclude_patterns) + + # Business workflow: Apply configuration + result = self._apply_file_watcher_configuration(enabled, debounce_seconds, additional_exclude_patterns) + + return result + + def _analyze_file_watcher_state(self) -> FileWatcherStatus: + """ + Business logic to analyze comprehensive file watcher state. + + Returns: + FileWatcherStatus with complete analysis + """ + # Business step 1: Check for error conditions + error_info = self._check_for_watcher_errors() + if error_info: + return self._create_error_status(error_info) + + # Business step 2: Check initialization state + watcher_service = self._watcher_tool.get_from_context() + if not watcher_service: + return self._create_not_initialized_status() + + # Business step 3: Get active status + return self._create_active_status(watcher_service) + + def _check_for_watcher_errors(self) -> Optional[Dict[str, Any]]: + """ + Business logic to check for file watcher error conditions. + + Returns: + Error information dictionary or None if no errors + """ + # Check context for recorded errors + if hasattr(self.ctx.request_context.lifespan_context, 'file_watcher_error'): + return self.ctx.request_context.lifespan_context.file_watcher_error + + return None + + def _create_error_status(self, error_info: Dict[str, Any]) -> FileWatcherStatus: + """ + Business logic to create error status with recommendations. + + Args: + error_info: Error information from context + + Returns: + FileWatcherStatus for error condition + """ + # Get configuration if available + configuration = self._get_file_watcher_configuration() + + # Get rebuild status + rebuild_status = self._get_rebuild_status() + + # Business logic: Generate error-specific recommendations + recommendations = [ + "Use refresh_index tool for manual updates", + "File watcher auto-refresh is disabled due to errors", + "Consider restarting the project or checking system permissions" + ] + + return FileWatcherStatus( + available=True, + active=False, + status="error", + message=error_info.get('message', 'File watcher error occurred'), + error_info=error_info, + configuration=configuration, + rebuild_status=rebuild_status, + recommendations=recommendations + ) + + def _create_not_initialized_status(self) -> FileWatcherStatus: + """ + Business logic to create not-initialized status. + + Returns: + FileWatcherStatus for not-initialized condition + """ + # Get basic configuration + configuration = self._get_file_watcher_configuration() + + # Get rebuild status + rebuild_status = self._get_rebuild_status() + + # Business logic: Generate initialization recommendations + recommendations = [ + "Use set_project_path tool to initialize file watcher", + "File monitoring will be enabled after project initialization" + ] + + return FileWatcherStatus( + available=True, + active=False, + status="not_initialized", + message="File watcher service not initialized. Set project path to enable auto-refresh.", + error_info=None, + configuration=configuration, + rebuild_status=rebuild_status, + recommendations=recommendations + ) + + def _create_active_status(self, watcher_service) -> FileWatcherStatus: + """ + Business logic to create active status with comprehensive information. + + Args: + watcher_service: Active file watcher service + + Returns: + FileWatcherStatus for active condition + """ + # Get detailed status from watcher service + watcher_status = watcher_service.get_status() + + # Get configuration + configuration = self._get_file_watcher_configuration() + + # Get rebuild status + rebuild_status = self._get_rebuild_status() + + # Business logic: Generate status-specific recommendations + recommendations = self._generate_active_recommendations(watcher_status) + + return FileWatcherStatus( + available=watcher_status.get('available', True), + active=watcher_status.get('active', False), + status=watcher_status.get('status', 'active'), + message=watcher_status.get('message'), + error_info=None, + configuration=configuration, + rebuild_status=rebuild_status, + recommendations=recommendations + ) + + def _get_file_watcher_configuration(self) -> Dict[str, Any]: + """ + Business logic to get file watcher configuration safely. + + Returns: + Configuration dictionary + """ + try: + # Try to get from project settings + if (hasattr(self.ctx.request_context.lifespan_context, 'settings') and + self.ctx.request_context.lifespan_context.settings): + return self.ctx.request_context.lifespan_context.settings.get_file_watcher_config() + + # Fallback to default configuration + return { + 'enabled': True, + 'debounce_seconds': 6.0, + 'additional_exclude_patterns': [], + 'note': 'Default configuration - project not fully initialized' + } + + except Exception as e: + return { + 'error': f'Could not load configuration: {e}', + 'enabled': True, + 'debounce_seconds': 6.0 + } + + def _get_rebuild_status(self) -> Dict[str, Any]: + """ + Business logic to get index rebuild status safely. + + Returns: + Rebuild status dictionary + """ + try: + index_service = IndexManagementService(self.ctx) + return index_service.get_rebuild_status() + + except Exception as e: + return { + 'status': 'unknown', + 'error': f'Could not get rebuild status: {e}' + } + + def _generate_active_recommendations(self, watcher_status: Dict[str, Any]) -> list[str]: + """ + Business logic to generate recommendations for active file watcher. + + Args: + watcher_status: Current watcher status + + Returns: + List of recommendations + """ + recommendations = [] + + if watcher_status.get('active', False): + recommendations.append("File watcher is active - automatic index updates enabled") + recommendations.append("Files will be re-indexed automatically when changed") + else: + recommendations.append("File watcher is available but not active") + recommendations.append("Use refresh_index for manual updates") + + # Add performance recommendations + restart_attempts = watcher_status.get('restart_attempts', 0) + if restart_attempts > 0: + recommendations.append(f"File watcher has restarted {restart_attempts} times - monitor for stability") + + return recommendations + + def _validate_configuration_request(self, enabled: Optional[bool], + debounce_seconds: Optional[float], + additional_exclude_patterns: Optional[list]) -> None: + """ + Business validation for file watcher configuration. + + Args: + enabled: Enable flag + debounce_seconds: Debounce time + additional_exclude_patterns: Exclude patterns + + Raises: + ValueError: If validation fails + """ + # Business rule: Enabled flag must be boolean if provided + if enabled is not None and not isinstance(enabled, bool): + raise ValueError("Enabled flag must be a boolean value") + + # Business rule: Debounce seconds must be reasonable + if debounce_seconds is not None: + if debounce_seconds < 0.1: + raise ValueError("Debounce seconds must be at least 0.1") + if debounce_seconds > 300: # 5 minutes + raise ValueError("Debounce seconds cannot exceed 300 (5 minutes)") + + # Business rule: Exclude patterns must be valid + if additional_exclude_patterns is not None: + if not isinstance(additional_exclude_patterns, list): + raise ValueError("Additional exclude patterns must be a list") + + for pattern in additional_exclude_patterns: + if not isinstance(pattern, str): + raise ValueError("All exclude patterns must be strings") + if not pattern.strip(): + raise ValueError("Exclude patterns cannot be empty") + + def _apply_file_watcher_configuration(self, enabled: Optional[bool], + debounce_seconds: Optional[float], + additional_exclude_patterns: Optional[list]) -> str: + """ + Business logic to apply file watcher configuration. + + Args: + enabled: Enable flag + debounce_seconds: Debounce time + additional_exclude_patterns: Exclude patterns + + Returns: + Success message + + Raises: + ValueError: If configuration cannot be applied + """ + # Business rule: Settings must be available + if (not hasattr(self.ctx.request_context.lifespan_context, 'settings') or + not self.ctx.request_context.lifespan_context.settings): + raise ValueError("Settings not available - project path not set") + + settings = self.ctx.request_context.lifespan_context.settings + + # Build updates dictionary + updates = {} + if enabled is not None: + updates["enabled"] = enabled + if debounce_seconds is not None: + updates["debounce_seconds"] = debounce_seconds + if additional_exclude_patterns is not None: + updates["additional_exclude_patterns"] = additional_exclude_patterns + + if not updates: + return "No configuration changes specified" + + # Apply configuration + settings.update_file_watcher_config(updates) + + # Business logic: Generate informative result message + changes_summary = [] + if 'enabled' in updates: + changes_summary.append(f"enabled={updates['enabled']}") + if 'debounce_seconds' in updates: + changes_summary.append(f"debounce={updates['debounce_seconds']}s") + if 'additional_exclude_patterns' in updates: + pattern_count = len(updates['additional_exclude_patterns']) + changes_summary.append(f"exclude_patterns={pattern_count}") + + changes_str = ", ".join(changes_summary) + + return (f"File watcher configuration updated: {changes_str}. " + f"Restart may be required for changes to take effect.") + + def _format_status_result(self, status_result: FileWatcherStatus) -> Dict[str, Any]: + """ + Format the status result according to business requirements. + + Args: + status_result: Status analysis result + + Returns: + Formatted result dictionary for MCP response + """ + result = { + 'available': status_result.available, + 'active': status_result.active, + 'status': status_result.status, + 'configuration': status_result.configuration, + 'rebuild_status': status_result.rebuild_status, + 'recommendations': status_result.recommendations + } + + # Add optional fields + if status_result.message: + result['message'] = status_result.message + + if status_result.error_info: + result['error'] = status_result.error_info + result['manual_refresh_required'] = True + + return result diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/__init__.py new file mode 100644 index 00000000..f69d664a --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/__init__.py @@ -0,0 +1,19 @@ +""" +Tool Layer - Technical components for the Code Index MCP server. + +This package contains pure technical components that provide specific +capabilities without business logic. These tools are composed by the +business layer to achieve business goals. +""" + +from .filesystem import FileMatchingTool, FileSystemTool +from .config import ProjectConfigTool, SettingsTool +from .monitoring import FileWatcherTool + +__all__ = [ + 'FileMatchingTool', + 'FileSystemTool', + 'ProjectConfigTool', + 'SettingsTool', + 'FileWatcherTool' +] diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/config/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/config/__init__.py new file mode 100644 index 00000000..12d43046 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/config/__init__.py @@ -0,0 +1,8 @@ +""" +Configuration Tools - Technical components for configuration management. +""" + +from .project_config_tool import ProjectConfigTool +from .settings_tool import SettingsTool + +__all__ = ['ProjectConfigTool', 'SettingsTool'] diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/config/project_config_tool.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/config/project_config_tool.py new file mode 100644 index 00000000..c2738dda --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/config/project_config_tool.py @@ -0,0 +1,308 @@ +""" +Project Configuration Tool - Pure technical component for project configuration operations. + +This tool handles low-level project configuration operations without any business logic. +""" + +import os +from typing import Dict, Any, Optional +from pathlib import Path + +from ...project_settings import ProjectSettings + + +class ProjectConfigTool: + """ + Pure technical component for project configuration operations. + + This tool provides low-level configuration management capabilities + without any business logic or decision making. + """ + + def __init__(self): + self._settings: Optional[ProjectSettings] = None + self._project_path: Optional[str] = None + + def initialize_settings(self, project_path: str) -> ProjectSettings: + """ + Initialize project settings for the given path. + + Args: + project_path: Absolute path to the project directory + + Returns: + ProjectSettings instance + + Raises: + ValueError: If project path is invalid + """ + if not Path(project_path).exists(): + raise ValueError(f"Project path does not exist: {project_path}") + + if not Path(project_path).is_dir(): + raise ValueError(f"Project path is not a directory: {project_path}") + + self._project_path = project_path + self._settings = ProjectSettings(project_path, skip_load=False) + + return self._settings + + def load_existing_index(self) -> Optional[Dict[str, Any]]: + """ + Load existing index data if available. + + Returns: + Index data dictionary or None if not available + + Raises: + RuntimeError: If settings not initialized + """ + if not self._settings: + raise RuntimeError("Settings not initialized. Call initialize_settings() first.") + + try: + return self._settings.load_index() + except Exception: + return None + + def save_project_config(self, config_data: Dict[str, Any]) -> None: + """ + Save project configuration data. + + Args: + config_data: Configuration data to save + + Raises: + RuntimeError: If settings not initialized + """ + if not self._settings: + raise RuntimeError("Settings not initialized") + + self._settings.save_config(config_data) + + def save_index_data(self, index_data: Dict[str, Any]) -> None: + """ + Save index data to persistent storage. + + Args: + index_data: Index data to save + + Raises: + RuntimeError: If settings not initialized + """ + if not self._settings: + raise RuntimeError("Settings not initialized") + + self._settings.save_index(index_data) + + def check_index_version(self) -> bool: + """ + Check if JSON index is the latest version. + + Returns: + True if JSON index exists and is recent, False if needs rebuild + + Raises: + RuntimeError: If settings not initialized + """ + if not self._settings: + raise RuntimeError("Settings not initialized") + + # Check if JSON index exists and is fresh + from ...indexing import get_index_manager + index_manager = get_index_manager() + + # Set project path if available + if self._settings.base_path: + index_manager.set_project_path(self._settings.base_path) + stats = index_manager.get_index_stats() + return stats.get('status') == 'loaded' + + return False + + def cleanup_legacy_files(self) -> None: + """ + Clean up legacy index files. + + Raises: + RuntimeError: If settings not initialized + """ + if not self._settings: + raise RuntimeError("Settings not initialized") + + self._settings.cleanup_legacy_files() + + def get_search_tool_info(self) -> Dict[str, Any]: + """ + Get information about available search tools. + + Returns: + Dictionary with search tool information + + Raises: + RuntimeError: If settings not initialized + """ + if not self._settings: + raise RuntimeError("Settings not initialized") + + search_tool = self._settings.get_preferred_search_tool() + return { + 'available': search_tool is not None, + 'name': search_tool.name if search_tool else None, + 'description': "Advanced search enabled" if search_tool else "Basic search available" + } + + def get_file_watcher_config(self) -> Dict[str, Any]: + """ + Get file watcher configuration. + + Returns: + File watcher configuration dictionary + + Raises: + RuntimeError: If settings not initialized + """ + if not self._settings: + raise RuntimeError("Settings not initialized") + + return self._settings.get_file_watcher_config() + + def create_default_config(self, project_path: str) -> Dict[str, Any]: + """ + Create default project configuration. + + Args: + project_path: Project path for the configuration + + Returns: + Default configuration dictionary + """ + from ...utils import FileFilter + + file_filter = FileFilter() + return { + "base_path": project_path, + "supported_extensions": list(file_filter.supported_extensions), + "last_indexed": None, + "file_watcher": self.get_file_watcher_config() if self._settings else {} + } + + def validate_project_path(self, path: str) -> Optional[str]: + """ + Validate project path. + + Args: + path: Path to validate + + Returns: + Error message if invalid, None if valid + """ + if not path or not path.strip(): + return "Project path cannot be empty" + + try: + norm_path = os.path.normpath(path) + abs_path = os.path.abspath(norm_path) + except (OSError, ValueError) as e: + return f"Invalid path format: {str(e)}" + + if not os.path.exists(abs_path): + return f"Path does not exist: {abs_path}" + + if not os.path.isdir(abs_path): + return f"Path is not a directory: {abs_path}" + + return None + + def normalize_project_path(self, path: str) -> str: + """ + Normalize and get absolute project path. + + Args: + path: Path to normalize + + Returns: + Normalized absolute path + """ + norm_path = os.path.normpath(path) + return os.path.abspath(norm_path) + + def get_settings_path(self) -> Optional[str]: + """ + Get the settings directory path. + + Returns: + Settings directory path or None if not initialized + """ + return self._settings.settings_path if self._settings else None + + def get_project_path(self) -> Optional[str]: + """ + Get the current project path. + + Returns: + Project path or None if not set + """ + return self._project_path + + def get_basic_project_structure(self, project_path: str) -> Dict[str, Any]: + """ + Get basic project directory structure. + + Args: + project_path: Path to analyze + + Returns: + Basic directory structure dictionary + """ + from ...utils import FileFilter + + file_filter = FileFilter() + + def build_tree(path: str, max_depth: int = 3, current_depth: int = 0) -> Dict[str, Any]: + """Build directory tree with limited depth using centralized filtering.""" + if current_depth >= max_depth: + return {"type": "directory", "truncated": True} + + try: + items = [] + path_obj = Path(path) + + for item in sorted(path_obj.iterdir()): + if item.is_dir(): + # Use centralized directory filtering + if not file_filter.should_exclude_directory(item.name): + items.append({ + "name": item.name, + "type": "directory", + "children": build_tree(str(item), max_depth, current_depth + 1) + }) + else: + # Use centralized file filtering + if not file_filter.should_exclude_file(item): + items.append({ + "name": item.name, + "type": "file", + "size": item.stat().st_size if item.exists() else 0 + }) + + return {"type": "directory", "children": items} + + except (OSError, PermissionError): + return {"type": "directory", "error": "Access denied"} + + try: + root_name = Path(project_path).name + structure = { + "name": root_name, + "path": project_path, + "type": "directory", + "children": build_tree(project_path)["children"] + } + return structure + + except Exception as e: + return { + "error": f"Failed to build project structure: {e}", + "path": project_path + } diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/config/settings_tool.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/config/settings_tool.py new file mode 100644 index 00000000..51fe0dce --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/config/settings_tool.py @@ -0,0 +1,100 @@ +""" +Settings Tool - Pure technical component for settings operations. + +This tool handles low-level settings operations without any business logic. +""" + +import os +import tempfile +from typing import Dict, Any + +from ...constants import SETTINGS_DIR + + +class SettingsTool: + """ + Pure technical component for settings operations. + + This tool provides low-level settings management capabilities + without any business logic or decision making. + """ + + def __init__(self): + pass + + def get_temp_directory_path(self) -> str: + """ + Get the path to the temporary directory for settings. + + Returns: + Path to the temporary settings directory + """ + return os.path.join(tempfile.gettempdir(), SETTINGS_DIR) + + def create_temp_directory(self) -> Dict[str, Any]: + """ + Create the temporary directory for settings. + + Returns: + Dictionary with creation results + """ + temp_dir = self.get_temp_directory_path() + existed_before = os.path.exists(temp_dir) + + try: + os.makedirs(temp_dir, exist_ok=True) + + return { + "temp_directory": temp_dir, + "exists": os.path.exists(temp_dir), + "is_directory": os.path.isdir(temp_dir), + "existed_before": existed_before, + "created": not existed_before + } + + except (OSError, IOError) as e: + return { + "temp_directory": temp_dir, + "exists": False, + "error": str(e) + } + + def check_temp_directory(self) -> Dict[str, Any]: + """ + Check the status of the temporary directory. + + Returns: + Dictionary with directory status information + """ + temp_dir = self.get_temp_directory_path() + + result = { + "temp_directory": temp_dir, + "temp_root": tempfile.gettempdir(), + "exists": os.path.exists(temp_dir), + "is_directory": os.path.isdir(temp_dir) if os.path.exists(temp_dir) else False + } + + # If the directory exists, list its contents + if result["exists"] and result["is_directory"]: + try: + contents = os.listdir(temp_dir) + result["contents"] = contents + result["subdirectories"] = [] + + # Check each subdirectory + for item in contents: + item_path = os.path.join(temp_dir, item) + if os.path.isdir(item_path): + subdir_info = { + "name": item, + "path": item_path, + "contents": os.listdir(item_path) if os.path.exists(item_path) else [] + } + result["subdirectories"].append(subdir_info) + + except (OSError, PermissionError) as e: + result["error"] = str(e) + + return result + diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/__init__.py new file mode 100644 index 00000000..e8f97982 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/__init__.py @@ -0,0 +1,8 @@ +""" +Filesystem Tools - Technical components for file system operations. +""" + +from .file_matching_tool import FileMatchingTool +from .file_system_tool import FileSystemTool + +__all__ = ['FileMatchingTool', 'FileSystemTool'] diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/file_matching_tool.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/file_matching_tool.py new file mode 100644 index 00000000..22ebdf6d --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/file_matching_tool.py @@ -0,0 +1,215 @@ +""" +File Matching Tool - Pure technical component for pattern matching operations. + +This tool handles file pattern matching without any business logic. +It provides technical capabilities for finding files based on various patterns. +""" + +import fnmatch +from typing import List, Set +from pathlib import Path + +# FileInfo defined locally for file matching operations +from dataclasses import dataclass + +@dataclass +class FileInfo: + """File information structure.""" + relative_path: str + language: str + + +class FileMatchingTool: + """ + Pure technical component for file pattern matching. + + This tool provides low-level pattern matching capabilities without + any business logic. It can match files using glob patterns, regex, + or other matching strategies. + """ + + def __init__(self): + pass + + def match_glob_pattern(self, files: List[FileInfo], pattern: str) -> List[FileInfo]: + """ + Match files using glob pattern. + + Args: + files: List of FileInfo objects to search through + pattern: Glob pattern (e.g., "*.py", "test_*.js", "src/**/*.ts") + + Returns: + List of FileInfo objects that match the pattern + """ + if not pattern: + return files + + matched_files = [] + + for file_info in files: + # Try matching against full path + if fnmatch.fnmatch(file_info.relative_path, pattern): + matched_files.append(file_info) + continue + + # Try matching against just the filename + filename = Path(file_info.relative_path).name + if fnmatch.fnmatch(filename, pattern): + matched_files.append(file_info) + + return matched_files + + def match_multiple_patterns(self, files: List[FileInfo], patterns: List[str]) -> List[FileInfo]: + """ + Match files using multiple glob patterns (OR logic). + + Args: + files: List of FileInfo objects to search through + patterns: List of glob patterns + + Returns: + List of FileInfo objects that match any of the patterns + """ + if not patterns: + return files + + matched_files = set() + + for pattern in patterns: + pattern_matches = self.match_glob_pattern(files, pattern) + matched_files.update(pattern_matches) + + return list(matched_files) + + def match_by_language(self, files: List[FileInfo], languages: List[str]) -> List[FileInfo]: + """ + Match files by programming language. + + Args: + files: List of FileInfo objects to search through + languages: List of language names (e.g., ["python", "javascript"]) + + Returns: + List of FileInfo objects with matching languages + """ + if not languages: + return files + + # Normalize language names for comparison + normalized_languages = {lang.lower() for lang in languages} + + matched_files = [] + for file_info in files: + if file_info.language.lower() in normalized_languages: + matched_files.append(file_info) + + return matched_files + + def match_by_directory(self, files: List[FileInfo], directory_patterns: List[str]) -> List[FileInfo]: + """ + Match files by directory patterns. + + Args: + files: List of FileInfo objects to search through + directory_patterns: List of directory patterns (e.g., ["src/*", "test/**"]) + + Returns: + List of FileInfo objects in matching directories + """ + if not directory_patterns: + return files + + matched_files = [] + + for file_info in files: + file_dir = str(Path(file_info.relative_path).parent) + + for dir_pattern in directory_patterns: + if fnmatch.fnmatch(file_dir, dir_pattern): + matched_files.append(file_info) + break + + return matched_files + + def exclude_patterns(self, files: List[FileInfo], exclude_patterns: List[str]) -> List[FileInfo]: + """ + Exclude files matching the given patterns. + + Args: + files: List of FileInfo objects to filter + exclude_patterns: List of patterns to exclude + + Returns: + List of FileInfo objects that don't match any exclude pattern + """ + if not exclude_patterns: + return files + + filtered_files = [] + + for file_info in files: + should_exclude = False + + for exclude_pattern in exclude_patterns: + if (fnmatch.fnmatch(file_info.relative_path, exclude_pattern) or + fnmatch.fnmatch(Path(file_info.relative_path).name, exclude_pattern)): + should_exclude = True + break + + if not should_exclude: + filtered_files.append(file_info) + + return filtered_files + + def sort_by_relevance(self, files: List[FileInfo], pattern: str) -> List[FileInfo]: + """ + Sort files by relevance to the search pattern. + + Args: + files: List of FileInfo objects to sort + pattern: Original search pattern for relevance scoring + + Returns: + List of FileInfo objects sorted by relevance (most relevant first) + """ + def relevance_score(file_info: FileInfo) -> int: + """Calculate relevance score for a file.""" + score = 0 + filename = Path(file_info.relative_path).name + + # Exact filename match gets highest score + if filename == pattern: + score += 100 + + # Filename starts with pattern + elif filename.startswith(pattern.replace('*', '')): + score += 50 + + # Pattern appears in filename + elif pattern.replace('*', '') in filename: + score += 25 + + # Shorter paths are generally more relevant + path_depth = len(Path(file_info.relative_path).parts) + score += max(0, 10 - path_depth) + + return score + + return sorted(files, key=relevance_score, reverse=True) + + def limit_results(self, files: List[FileInfo], max_results: int) -> List[FileInfo]: + """ + Limit the number of results returned. + + Args: + files: List of FileInfo objects + max_results: Maximum number of results to return + + Returns: + List of FileInfo objects limited to max_results + """ + if max_results <= 0: + return files + + return files[:max_results] diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/file_system_tool.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/file_system_tool.py new file mode 100644 index 00000000..fac9f5d3 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/filesystem/file_system_tool.py @@ -0,0 +1,234 @@ +""" +File System Tool - Pure technical component for file system operations. + +This tool handles low-level file system operations without any business logic. +""" + +import os +from typing import Dict, Any, Optional +from pathlib import Path + + +class FileSystemTool: + """ + Pure technical component for file system operations. + + This tool provides low-level file system capabilities without + any business logic or decision making. + """ + + def __init__(self): + pass + + def get_file_stats(self, file_path: str) -> Dict[str, Any]: + """ + Get basic file system statistics for a file. + + Args: + file_path: Absolute path to the file + + Returns: + Dictionary with file statistics + + Raises: + FileNotFoundError: If file doesn't exist + OSError: If file cannot be accessed + """ + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + try: + stat_info = os.stat(file_path) + path_obj = Path(file_path) + + return { + 'size_bytes': stat_info.st_size, + 'modified_time': stat_info.st_mtime, + 'created_time': stat_info.st_ctime, + 'is_file': path_obj.is_file(), + 'is_directory': path_obj.is_dir(), + 'extension': path_obj.suffix, + 'name': path_obj.name, + 'parent': str(path_obj.parent) + } + + except OSError as e: + raise OSError(f"Cannot access file {file_path}: {e}") from e + + def read_file_content(self, file_path: str) -> str: + """ + Read file content with intelligent encoding detection. + + Args: + file_path: Absolute path to the file + + Returns: + File content as string + + Raises: + FileNotFoundError: If file doesn't exist + ValueError: If file cannot be decoded + """ + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + # Try UTF-8 first (most common) + try: + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + except UnicodeDecodeError: + pass + + # Try other common encodings + encodings = ['utf-8-sig', 'latin-1', 'cp1252', 'iso-8859-1'] + for encoding in encodings: + try: + with open(file_path, 'r', encoding=encoding) as f: + return f.read() + except UnicodeDecodeError: + continue + + raise ValueError(f"Could not decode file {file_path} with any supported encoding") + + def count_lines(self, file_path: str) -> int: + """ + Count the number of lines in a file. + + Args: + file_path: Absolute path to the file + + Returns: + Number of lines in the file + + Raises: + FileNotFoundError: If file doesn't exist + """ + try: + content = self.read_file_content(file_path) + return len(content.splitlines()) + except Exception: + # If we can't read the file, return 0 + return 0 + + def detect_language_from_extension(self, file_path: str) -> str: + """ + Detect programming language from file extension. + + Args: + file_path: Path to the file + + Returns: + Language name or 'unknown' + """ + extension = Path(file_path).suffix.lower() + + lang_map = { + '.py': 'python', + '.js': 'javascript', + '.jsx': 'javascript', + '.ts': 'typescript', + '.tsx': 'typescript', + '.java': 'java', + '.cpp': 'cpp', + '.cxx': 'cpp', + '.cc': 'cpp', + '.c': 'c', + '.h': 'c', + '.hpp': 'cpp', + '.hxx': 'cpp', + '.cs': 'csharp', + '.go': 'go', + '.rs': 'rust', + '.php': 'php', + '.rb': 'ruby', + '.swift': 'swift', + '.kt': 'kotlin', + '.scala': 'scala', + '.m': 'objc', + '.mm': 'objc', + '.html': 'html', + '.htm': 'html', + '.css': 'css', + '.scss': 'scss', + '.sass': 'sass', + '.less': 'less', + '.json': 'json', + '.xml': 'xml', + '.yaml': 'yaml', + '.yml': 'yaml', + '.md': 'markdown', + '.txt': 'text', + '.sh': 'shell', + '.bash': 'shell', + '.zsh': 'shell', + '.fish': 'shell', + '.ps1': 'powershell', + '.bat': 'batch', + '.cmd': 'batch' + } + + return lang_map.get(extension, 'unknown') + + def is_text_file(self, file_path: str) -> bool: + """ + Check if a file is likely a text file. + + Args: + file_path: Path to the file + + Returns: + True if file appears to be text, False otherwise + """ + try: + # Try to read a small portion of the file + with open(file_path, 'rb') as f: + chunk = f.read(1024) + + # Check for null bytes (common in binary files) + if b'\x00' in chunk: + return False + + # Try to decode as UTF-8 + try: + chunk.decode('utf-8') + return True + except UnicodeDecodeError: + # Try other encodings + for encoding in ['latin-1', 'cp1252']: + try: + chunk.decode(encoding) + return True + except UnicodeDecodeError: + continue + + return False + + except Exception: + return False + + def get_file_size_category(self, file_path: str) -> str: + """ + Categorize file size for analysis purposes. + + Args: + file_path: Path to the file + + Returns: + Size category: 'small', 'medium', 'large', or 'very_large' + """ + try: + size = os.path.getsize(file_path) + + if size < 1024: # < 1KB + return 'tiny' + elif size < 10 * 1024: # < 10KB + return 'small' + elif size < 100 * 1024: # < 100KB + return 'medium' + elif size < 1024 * 1024: # < 1MB + return 'large' + else: + return 'very_large' + + except Exception: + return 'unknown' diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/monitoring/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/monitoring/__init__.py new file mode 100644 index 00000000..6da231e1 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/monitoring/__init__.py @@ -0,0 +1,7 @@ +""" +Monitoring Tools - Technical components for file monitoring operations. +""" + +from .file_watcher_tool import FileWatcherTool + +__all__ = ['FileWatcherTool'] \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/tools/monitoring/file_watcher_tool.py b/reference/code-index-mcp-master/src/code_index_mcp/tools/monitoring/file_watcher_tool.py new file mode 100644 index 00000000..3671952e --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/tools/monitoring/file_watcher_tool.py @@ -0,0 +1,134 @@ +""" +File Watcher Tool - Pure technical component for file monitoring operations. + +This tool handles low-level file watching operations without any business logic. +""" + +import time +from typing import Optional, Callable +from ...utils import ContextHelper +from ...services.file_watcher_service import FileWatcherService + + +class FileWatcherTool: + """ + Pure technical component for file monitoring operations. + + This tool provides low-level file watching capabilities without + any business logic or decision making. + """ + + def __init__(self, ctx): + self._ctx = ctx + self._file_watcher_service: Optional[FileWatcherService] = None + + + def create_watcher(self) -> FileWatcherService: + """ + Create a new file watcher service instance. + + Returns: + FileWatcherService instance + """ + self._file_watcher_service = FileWatcherService(self._ctx) + return self._file_watcher_service + + def start_monitoring(self, project_path: str, rebuild_callback: Callable) -> bool: + """ + Start file monitoring for the given project path. + + Args: + project_path: Path to monitor + rebuild_callback: Callback function for rebuild events + + Returns: + True if monitoring started successfully, False otherwise + """ + if not self._file_watcher_service: + self._file_watcher_service = self.create_watcher() + + # Validate that the project path matches the expected base path + helper = ContextHelper(self._ctx) + if helper.base_path and helper.base_path != project_path: + pass + + return self._file_watcher_service.start_monitoring(rebuild_callback) + + def stop_monitoring(self) -> None: + """Stop file monitoring if active.""" + if self._file_watcher_service: + self._file_watcher_service.stop_monitoring() + + def is_monitoring_active(self) -> bool: + """ + Check if file monitoring is currently active. + + Returns: + True if monitoring is active, False otherwise + """ + return (self._file_watcher_service is not None and + self._file_watcher_service.is_active()) + + def get_monitoring_status(self) -> dict: + """ + Get current monitoring status. + + Returns: + Dictionary with monitoring status information + """ + if not self._file_watcher_service: + return { + 'active': False, + 'available': True, + 'status': 'not_initialized' + } + + return self._file_watcher_service.get_status() + + def store_in_context(self) -> None: + """Store the file watcher service in the MCP context.""" + if (self._file_watcher_service and + hasattr(self._ctx.request_context.lifespan_context, '__dict__')): + self._ctx.request_context.lifespan_context.file_watcher_service = self._file_watcher_service + + def get_from_context(self) -> Optional[FileWatcherService]: + """ + Get existing file watcher service from context. + + Returns: + FileWatcherService instance or None if not found + """ + if hasattr(self._ctx.request_context.lifespan_context, 'file_watcher_service'): + return self._ctx.request_context.lifespan_context.file_watcher_service + return None + + def stop_existing_watcher(self) -> None: + """Stop any existing file watcher from context.""" + existing_watcher = self.get_from_context() + if existing_watcher: + + existing_watcher.stop_monitoring() + # Clear reference + if hasattr(self._ctx.request_context.lifespan_context, '__dict__'): + self._ctx.request_context.lifespan_context.file_watcher_service = None + + + def record_error(self, error_message: str) -> None: + """ + Record file watcher error in context for status reporting. + + Args: + error_message: Error message to record + """ + error_info = { + 'status': 'failed', + 'message': f'{error_message}. Auto-refresh disabled. Please use manual refresh.', + 'timestamp': time.time(), + 'manual_refresh_required': True + } + + # Store error in context for status reporting + if hasattr(self._ctx.request_context.lifespan_context, '__dict__'): + self._ctx.request_context.lifespan_context.file_watcher_error = error_info + + diff --git a/reference/code-index-mcp-master/src/code_index_mcp/utils/__init__.py b/reference/code-index-mcp-master/src/code_index_mcp/utils/__init__.py new file mode 100644 index 00000000..acb48dbc --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/utils/__init__.py @@ -0,0 +1,31 @@ +""" +Utility modules for the Code Index MCP server. + +This package contains shared utilities used across services: +- error_handler: Decorator-based error handling for MCP entry points +- context_helper: Context access utilities and helpers +- validation: Common validation logic +- response_formatter: Response formatting utilities +""" + +from .error_handler import ( + handle_mcp_errors, + handle_mcp_resource_errors, + handle_mcp_tool_errors, + MCPToolError, +) +from .context_helper import ContextHelper +from .validation import ValidationHelper +from .response_formatter import ResponseFormatter +from .file_filter import FileFilter + +__all__ = [ + 'handle_mcp_errors', + 'handle_mcp_resource_errors', + 'handle_mcp_tool_errors', + 'MCPToolError', + 'ContextHelper', + 'ValidationHelper', + 'ResponseFormatter', + 'FileFilter' +] diff --git a/reference/code-index-mcp-master/src/code_index_mcp/utils/context_helper.py b/reference/code-index-mcp-master/src/code_index_mcp/utils/context_helper.py new file mode 100644 index 00000000..1ed5fa6f --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/utils/context_helper.py @@ -0,0 +1,169 @@ +""" +Context access utilities and helpers. + +This module provides convenient access to MCP Context data and common +operations that services need to perform with the context. +""" + +import os +from typing import Optional +from mcp.server.fastmcp import Context + +from ..project_settings import ProjectSettings + + +class ContextHelper: + """ + Helper class for convenient access to MCP Context data. + + This class wraps the MCP Context object and provides convenient properties + and methods for accessing commonly needed data like base_path, settings, etc. + """ + + def __init__(self, ctx: Context): + """ + Initialize the context helper. + + Args: + ctx: The MCP Context object + """ + self.ctx = ctx + + @property + def base_path(self) -> str: + """ + Get the base project path from the context. + + Returns: + The base project path, or empty string if not set + """ + try: + return self.ctx.request_context.lifespan_context.base_path + except AttributeError: + return "" + + @property + def settings(self) -> Optional[ProjectSettings]: + """ + Get the project settings from the context. + + Returns: + The ProjectSettings instance, or None if not available + """ + try: + return self.ctx.request_context.lifespan_context.settings + except AttributeError: + return None + + @property + def file_count(self) -> int: + """ + Get the current file count from the context. + + Returns: + The number of indexed files, or 0 if not available + """ + try: + return self.ctx.request_context.lifespan_context.file_count + except AttributeError: + return 0 + + @property + def index_manager(self): + """ + Get the unified index manager from the context. + + Returns: + The UnifiedIndexManager instance, or None if not available + """ + try: + return getattr(self.ctx.request_context.lifespan_context, 'index_manager', None) + except AttributeError: + return None + + def validate_base_path(self) -> bool: + """ + Check if the base path is set and valid. + + Returns: + True if base path is set and exists, False otherwise + """ + base_path = self.base_path + return bool(base_path and os.path.exists(base_path)) + + def get_base_path_error(self) -> Optional[str]: + """ + Get an error message if base path is not properly set. + + Returns: + Error message string if base path is invalid, None if valid + """ + if not self.base_path: + return ("Project path not set. Please use set_project_path to set a " + "project directory first.") + + if not os.path.exists(self.base_path): + return f"Project path does not exist: {self.base_path}" + + if not os.path.isdir(self.base_path): + return f"Project path is not a directory: {self.base_path}" + + return None + + def update_file_count(self, count: int) -> None: + """ + Update the file count in the context. + + Args: + count: The new file count + """ + try: + self.ctx.request_context.lifespan_context.file_count = count + except AttributeError: + pass # Context not available or doesn't support this operation + + def update_base_path(self, path: str) -> None: + """ + Update the base path in the context. + + Args: + path: The new base path + """ + try: + self.ctx.request_context.lifespan_context.base_path = path + except AttributeError: + pass # Context not available or doesn't support this operation + + def update_settings(self, settings: ProjectSettings) -> None: + """ + Update the settings in the context. + + Args: + settings: The new ProjectSettings instance + """ + try: + self.ctx.request_context.lifespan_context.settings = settings + except AttributeError: + pass # Context not available or doesn't support this operation + + def clear_index_cache(self) -> None: + """ + Clear the index through the unified index manager. + """ + try: + if self.index_manager: + self.index_manager.clear_index() + except AttributeError: + pass + + def update_index_manager(self, index_manager) -> None: + """ + Update the index manager in the context. + + Args: + index_manager: The new UnifiedIndexManager instance + """ + try: + self.ctx.request_context.lifespan_context.index_manager = index_manager + except AttributeError: + pass # Context not available or doesn't support this operation diff --git a/reference/code-index-mcp-master/src/code_index_mcp/utils/error_handler.py b/reference/code-index-mcp-master/src/code_index_mcp/utils/error_handler.py new file mode 100644 index 00000000..91b924a2 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/utils/error_handler.py @@ -0,0 +1,122 @@ +""" +Decorator-based error handling for MCP entry points. + +This module provides consistent error handling across all MCP tools, resources, and prompts. +""" + +import functools +import json +from typing import Any, Callable + + +class MCPToolError(RuntimeError): + """Exception raised when an MCP entry point fails.""" + + def __init__(self, message: str): + super().__init__(message) + + +def handle_mcp_errors(return_type: str = 'str') -> Callable: + """ + Decorator to handle exceptions in MCP entry points consistently. + + This decorator catches all exceptions and rethrows them as MCPToolError after + formatting a consistent error message. FastMCP converts the raised exception + into a structured error response for the client. + + Args: + return_type: Label used to format the error message for logging/consistency. + - 'str'/'list'/others: Prefixes message with "Error: ..." + - 'dict'/'json': Prefixes message with "Operation failed: ..." + + Returns: + Decorator function that wraps MCP entry points with error handling + + Example: + @mcp.tool() + @handle_mcp_errors(return_type='str') + def set_project_path(path: str, ctx: Context) -> str: + from ..services.project_management_service import ProjectManagementService + return ProjectManagementService(ctx).initialize_project(path) + + @mcp.tool() + @handle_mcp_errors(return_type='dict') + def search_code_advanced(pattern: str, ctx: Context, **kwargs) -> Dict[str, Any]: + return SearchService(ctx).search_code(pattern, **kwargs) + """ + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs) -> Any: + try: + return func(*args, **kwargs) + except MCPToolError: + raise + except Exception as exc: + error_message = str(exc) + formatted = _format_error_message(error_message, return_type) + raise MCPToolError(formatted) from exc + + return wrapper + return decorator + + +def handle_mcp_resource_errors(func: Callable) -> Callable: + """ + Specialized error handler for MCP resources that always return strings. + + This is a convenience decorator specifically for @mcp.resource decorated functions + which always return string responses. + + Args: + func: The MCP resource function to wrap + + Returns: + Wrapped function with error handling + + Example: + @mcp.resource("config://code-indexer") + @handle_mcp_resource_errors + def get_config(ctx: Context) -> str: + from ..services.project_management_service import ProjectManagementService + return ProjectManagementService(ctx).get_project_config() + """ + return handle_mcp_errors(return_type='str')(func) + + +def handle_mcp_tool_errors(return_type: str = 'str') -> Callable: + """ + Specialized error handler for MCP tools with flexible return types. + + This is a convenience decorator specifically for @mcp.tool decorated functions + which may return either strings or dictionaries. + + Args: + return_type: Label describing the successful payload shape (e.g. 'str', 'dict', 'list'). + + Returns: + Decorator function for MCP tools + + Example: + @mcp.tool() + @handle_mcp_tool_errors(return_type='dict') + def find_files(pattern: str, ctx: Context) -> Dict[str, Any]: + from ..services.file_discovery_service import FileDiscoveryService + return FileDiscoveryService(ctx).find_files(pattern) + """ + return handle_mcp_errors(return_type=return_type) + + +def _format_error_message(error_message: str, return_type: str) -> str: + """ + Convert an exception message into a consistent string for MCP errors. + + Args: + error_message: The raw exception message. + return_type: The declared return type for the decorated entry point. + + Returns: + A string representation suitable for raising as MCPToolError. + """ + if return_type in {'dict', 'json'}: + return f"Operation failed: {error_message}" + return f"Error: {error_message}" diff --git a/reference/code-index-mcp-master/src/code_index_mcp/utils/file_filter.py b/reference/code-index-mcp-master/src/code_index_mcp/utils/file_filter.py new file mode 100644 index 00000000..5cd99382 --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/utils/file_filter.py @@ -0,0 +1,177 @@ +""" +Centralized file filtering logic for the Code Index MCP server. + +This module provides unified filtering capabilities used across all components +that need to determine which files and directories should be processed or excluded. +""" + +import fnmatch +from pathlib import Path +from typing import List, Optional, Set + +from ..constants import FILTER_CONFIG + + +class FileFilter: + """Centralized file filtering logic.""" + + def __init__(self, additional_excludes: Optional[List[str]] = None): + """ + Initialize the file filter. + + Args: + additional_excludes: Additional directory patterns to exclude + """ + self.exclude_dirs = set(FILTER_CONFIG["exclude_directories"]) + self.exclude_files = set(FILTER_CONFIG["exclude_files"]) + self.supported_extensions = set(FILTER_CONFIG["supported_extensions"]) + + # Add user-defined exclusions + if additional_excludes: + self.exclude_dirs.update(additional_excludes) + + def should_exclude_directory(self, dir_name: str) -> bool: + """ + Check if directory should be excluded from processing. + + Args: + dir_name: Directory name to check + + Returns: + True if directory should be excluded, False otherwise + """ + # Skip hidden directories except for specific allowed ones + if dir_name.startswith('.') and dir_name not in {'.env', '.gitignore'}: + return True + + # Check against exclude patterns + return dir_name in self.exclude_dirs + + def should_exclude_file(self, file_path: Path) -> bool: + """ + Check if file should be excluded from processing. + + Args: + file_path: Path object for the file to check + + Returns: + True if file should be excluded, False otherwise + """ + # Extension check - only process supported file types + if file_path.suffix.lower() not in self.supported_extensions: + return True + + # Hidden files (except specific allowed ones) + if file_path.name.startswith('.') and file_path.name not in {'.gitignore', '.env'}: + return True + + # Filename pattern check using glob patterns + for pattern in self.exclude_files: + if fnmatch.fnmatch(file_path.name, pattern): + return True + + return False + + def should_process_path(self, path: Path, base_path: Path) -> bool: + """ + Unified path processing logic to determine if a file should be processed. + + Args: + path: File path to check + base_path: Project base path for relative path calculation + + Returns: + True if file should be processed, False otherwise + """ + try: + # Ensure we're working with absolute paths + if not path.is_absolute(): + path = base_path / path + + # Get relative path from base + relative_path = path.relative_to(base_path) + + # Check each path component for excluded directories + for part in relative_path.parts[:-1]: # Exclude filename + if self.should_exclude_directory(part): + return False + + # Check file itself + return not self.should_exclude_file(path) + + except (ValueError, OSError): + # Path not relative to base_path or other path errors + return False + + def is_supported_file_type(self, file_path: Path) -> bool: + """ + Check if file type is supported for indexing. + + Args: + file_path: Path to check + + Returns: + True if file type is supported, False otherwise + """ + return file_path.suffix.lower() in self.supported_extensions + + def is_temporary_file(self, file_path: Path) -> bool: + """ + Check if file appears to be a temporary file. + + Args: + file_path: Path to check + + Returns: + True if file appears temporary, False otherwise + """ + name = file_path.name + + # Common temporary file patterns + temp_patterns = ['*.tmp', '*.temp', '*.swp', '*.swo', '*~'] + + for pattern in temp_patterns: + if fnmatch.fnmatch(name, pattern): + return True + + # Files ending in .bak or .orig + if name.endswith(('.bak', '.orig')): + return True + + return False + + def filter_file_list(self, files: List[str], base_path: str) -> List[str]: + """ + Filter a list of file paths, keeping only those that should be processed. + + Args: + files: List of file paths (absolute or relative) + base_path: Project base path + + Returns: + Filtered list of file paths that should be processed + """ + base = Path(base_path) + filtered = [] + + for file_path_str in files: + file_path = Path(file_path_str) + if self.should_process_path(file_path, base): + filtered.append(file_path_str) + + return filtered + + def get_exclude_summary(self) -> dict: + """ + Get summary of current exclusion configuration. + + Returns: + Dictionary with exclusion configuration details + """ + return { + "exclude_directories_count": len(self.exclude_dirs), + "exclude_files_count": len(self.exclude_files), + "supported_extensions_count": len(self.supported_extensions), + "exclude_directories": sorted(self.exclude_dirs), + "exclude_files": sorted(self.exclude_files) + } \ No newline at end of file diff --git a/reference/code-index-mcp-master/src/code_index_mcp/utils/response_formatter.py b/reference/code-index-mcp-master/src/code_index_mcp/utils/response_formatter.py new file mode 100644 index 00000000..4e12a36d --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/utils/response_formatter.py @@ -0,0 +1,372 @@ +""" +Response formatting utilities for the MCP server. + +This module provides consistent response formatting functions used across +services to ensure uniform response structures and formats. +""" + +import json +from typing import Any, Dict, List, Optional, Union + +from ..indexing.qualified_names import generate_qualified_name + + +class ResponseFormatter: + """ + Helper class for formatting responses consistently across services. + + This class provides static methods for formatting different types of + responses in a consistent manner. + """ + + @staticmethod + def _resolve_qualified_names_in_relationships( + file_path: str, + relationship_list: List[str], + duplicate_names: set, + index_cache: Optional[Dict[str, Any]] = None + ) -> List[str]: + """ + Convert simple names to qualified names when duplicates exist. + + Args: + file_path: Current file path for context + relationship_list: List of function/class names that may need qualification + duplicate_names: Set of names that have duplicates in the project + index_cache: Optional index cache for duplicate detection + + Returns: + List with qualified names where duplicates exist + """ + if not relationship_list or not duplicate_names: + return relationship_list + + qualified_list = [] + for name in relationship_list: + if name in duplicate_names: + # Convert to qualified name if this name has duplicates + if index_cache and 'files' in index_cache: + # Try to find the actual file where this name is defined + # For now, we'll use the current file path as context + qualified_name = generate_qualified_name(file_path, name) + qualified_list.append(qualified_name) + else: + # Fallback: keep original name if we can't resolve + qualified_list.append(name) + else: + # No duplicates, keep original name + qualified_list.append(name) + + return qualified_list + + @staticmethod + def _get_duplicate_names_from_index(index_cache: Optional[Dict[str, Any]] = None) -> Dict[str, set]: + """ + Extract duplicate function and class names from index cache. + + Args: + index_cache: Optional index cache + + Returns: + Dictionary with 'functions' and 'classes' sets of duplicate names + """ + duplicates = {'functions': set(), 'classes': set()} + + if not index_cache: + return duplicates + + # Duplicate detection functionality removed - was legacy code + # Return empty duplicates as this feature is no longer used + + return duplicates + + @staticmethod + def success_response(message: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + Format a successful operation response. + + Args: + message: Success message + data: Optional additional data to include + + Returns: + Formatted success response dictionary + """ + response = {"status": "success", "message": message} + if data: + response.update(data) + return response + + @staticmethod + def error_response(message: str, error_code: Optional[str] = None) -> Dict[str, Any]: + """ + Format an error response. + + Args: + message: Error message + error_code: Optional error code for categorization + + Returns: + Formatted error response dictionary + """ + response = {"error": message} + if error_code: + response["error_code"] = error_code + return response + + @staticmethod + def file_list_response(files: List[str], status_message: str) -> Dict[str, Any]: + """ + Format a file list response for find_files operations. + + Args: + files: List of file paths + status_message: Status message describing the operation result + + Returns: + Formatted file list response + """ + return { + "files": files, + "status": status_message + } + + @staticmethod + def search_results_response( + results: List[Dict[str, Any]], + pagination: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Format search results response. + + Args: + results: List of search result dictionaries + + Returns: + Formatted search results response + """ + response = { + "results": results + } + + if pagination is not None: + response["pagination"] = pagination + + return response + + @staticmethod + def config_response(config_data: Dict[str, Any]) -> str: + """ + Format configuration data as JSON string. + + Args: + config_data: Configuration data dictionary + + Returns: + JSON formatted configuration string + """ + return json.dumps(config_data, indent=2) + + @staticmethod + def stats_response(stats_data: Dict[str, Any]) -> str: + """ + Format statistics data as JSON string. + + Args: + stats_data: Statistics data dictionary + + Returns: + JSON formatted statistics string + """ + return json.dumps(stats_data, indent=2) + + @staticmethod + def file_summary_response( + file_path: str, + line_count: int, + size_bytes: int, + extension: str, + language: str = "unknown", + functions: Optional[Union[List[str], List[Dict[str, Any]]]] = None, + classes: Optional[Union[List[str], List[Dict[str, Any]]]] = None, + imports: Optional[Union[List[str], List[Dict[str, Any]]]] = None, + language_specific: Optional[Dict[str, Any]] = None, + error: Optional[str] = None, + index_cache: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Format file summary response from index data. + + Args: + file_path: Path to the file + line_count: Number of lines in the file + size_bytes: File size in bytes + extension: File extension + language: Programming language detected + functions: List of function names (strings) or complete function objects (dicts) + classes: List of class names (strings) or complete class objects (dicts) + imports: List of import statements (strings) or complete import objects (dicts) + language_specific: Language-specific analysis data + error: Error message if analysis failed + index_cache: Optional index cache for duplicate name resolution + + Returns: + Formatted file summary response + """ + # Get duplicate names from index for qualified name resolution + duplicate_names = ResponseFormatter._get_duplicate_names_from_index(index_cache) + + # Handle backward compatibility for functions + processed_functions = [] + if functions: + for func in functions: + if isinstance(func, str): + # Legacy format - convert string to basic object + processed_functions.append({"name": func}) + elif isinstance(func, dict): + # New format - use complete object and resolve qualified names in relationships + processed_func = func.copy() + + # Resolve qualified names in relationship fields + if 'calls' in processed_func and isinstance(processed_func['calls'], list): + processed_func['calls'] = ResponseFormatter._resolve_qualified_names_in_relationships( + file_path, processed_func['calls'], duplicate_names['functions'], index_cache + ) + + if 'called_by' in processed_func and isinstance(processed_func['called_by'], list): + processed_func['called_by'] = ResponseFormatter._resolve_qualified_names_in_relationships( + file_path, processed_func['called_by'], duplicate_names['functions'], index_cache + ) + + processed_functions.append(processed_func) + + # Handle backward compatibility for classes + processed_classes = [] + if classes: + for cls in classes: + if isinstance(cls, str): + # Legacy format - convert string to basic object + processed_classes.append({"name": cls}) + elif isinstance(cls, dict): + # New format - use complete object and resolve qualified names in relationships + processed_cls = cls.copy() + + # Resolve qualified names in relationship fields + if 'instantiated_by' in processed_cls and isinstance(processed_cls['instantiated_by'], list): + processed_cls['instantiated_by'] = ResponseFormatter._resolve_qualified_names_in_relationships( + file_path, processed_cls['instantiated_by'], duplicate_names['functions'], index_cache + ) + + processed_classes.append(processed_cls) + + # Handle backward compatibility for imports + processed_imports = [] + if imports: + for imp in imports: + if isinstance(imp, str): + # Legacy format - convert string to basic object + processed_imports.append({"module": imp, "import_type": "unknown"}) + elif isinstance(imp, dict): + # New format - use complete object + processed_imports.append(imp) + + response = { + "file_path": file_path, + "line_count": line_count, + "size_bytes": size_bytes, + "extension": extension, + "language": language, + "functions": processed_functions, + "classes": processed_classes, + "imports": processed_imports, + "language_specific": language_specific or {} + } + + if error: + response["error"] = error + + return response + + @staticmethod + def directory_info_response( + temp_directory: str, + exists: bool, + is_directory: bool = False, + contents: Optional[List[str]] = None, + subdirectories: Optional[List[Dict[str, Any]]] = None, + error: Optional[str] = None + ) -> Dict[str, Any]: + """ + Format directory information response. + + Args: + temp_directory: Path to the directory + exists: Whether the directory exists + is_directory: Whether the path is a directory + contents: List of directory contents + subdirectories: List of subdirectory information + error: Error message if operation failed + + Returns: + Formatted directory info response + """ + response = { + "temp_directory": temp_directory, + "exists": exists, + "is_directory": is_directory + } + + if contents is not None: + response["contents"] = contents + + if subdirectories is not None: + response["subdirectories"] = subdirectories + + if error: + response["error"] = error + + return response + + @staticmethod + def settings_info_response( + settings_directory: str, + temp_directory: str, + temp_directory_exists: bool, + config: Dict[str, Any], + stats: Dict[str, Any], + exists: bool, + status: str = "configured", + message: Optional[str] = None + ) -> Dict[str, Any]: + """ + Format settings information response. + + Args: + settings_directory: Path to settings directory + temp_directory: Path to temp directory + temp_directory_exists: Whether temp directory exists + config: Configuration data + stats: Statistics data + exists: Whether settings directory exists + status: Status of the configuration + message: Optional status message + + Returns: + Formatted settings info response + """ + response = { + "settings_directory": settings_directory, + "temp_directory": temp_directory, + "temp_directory_exists": temp_directory_exists, + "config": config, + "stats": stats, + "exists": exists + } + + if status != "configured": + response["status"] = status + + if message: + response["message"] = message + + return response diff --git a/reference/code-index-mcp-master/src/code_index_mcp/utils/validation.py b/reference/code-index-mcp-master/src/code_index_mcp/utils/validation.py new file mode 100644 index 00000000..8c4fe87c --- /dev/null +++ b/reference/code-index-mcp-master/src/code_index_mcp/utils/validation.py @@ -0,0 +1,239 @@ +""" +Common validation logic for the MCP server. + +This module provides shared validation functions used across services +to ensure consistent validation behavior and reduce code duplication. +""" + +import os +import re +import fnmatch +from typing import Optional, List + +from ..indexing.qualified_names import normalize_file_path + + +class ValidationHelper: + """ + Helper class containing common validation logic. + + This class provides static methods for common validation operations + that are used across multiple services. + """ + + @staticmethod + def validate_file_path(file_path: str, base_path: str) -> Optional[str]: + """ + Validate a file path for security and accessibility. + + This method checks for: + - Path traversal attempts + - Absolute path usage (not allowed) + - Path existence within base directory + + Args: + file_path: The file path to validate (should be relative) + base_path: The base project directory path + + Returns: + Error message if validation fails, None if valid + """ + if not file_path: + return "File path cannot be empty" + + if not base_path: + return "Base path not set" + + # Handle absolute paths (especially Windows paths starting with drive letters) + if os.path.isabs(file_path) or (len(file_path) > 1 and file_path[1] == ':'): + return (f"Absolute file paths like '{file_path}' are not allowed. " + "Please use paths relative to the project root.") + + # Normalize the file path + norm_path = os.path.normpath(file_path) + + # Check for path traversal attempts + if "..\\" in norm_path or "../" in norm_path or norm_path.startswith(".."): + return f"Invalid file path: {file_path} (directory traversal not allowed)" + + # Construct the full path and verify it's within the project bounds + full_path = os.path.join(base_path, norm_path) + real_full_path = os.path.realpath(full_path) + real_base_path = os.path.realpath(base_path) + + if not real_full_path.startswith(real_base_path): + return "Access denied. File path must be within project directory." + + return None + + @staticmethod + def validate_directory_path(dir_path: str) -> Optional[str]: + """ + Validate a directory path for project initialization. + + Args: + dir_path: The directory path to validate + + Returns: + Error message if validation fails, None if valid + """ + if not dir_path: + return "Directory path cannot be empty" + + # Normalize and get absolute path + try: + norm_path = os.path.normpath(dir_path) + abs_path = os.path.abspath(norm_path) + except (OSError, ValueError) as e: + return f"Invalid path format: {str(e)}" + + if not os.path.exists(abs_path): + return f"Path does not exist: {abs_path}" + + if not os.path.isdir(abs_path): + return f"Path is not a directory: {abs_path}" + + return None + + @staticmethod + def validate_glob_pattern(pattern: str) -> Optional[str]: + """ + Validate a glob pattern for file searching. + + Args: + pattern: The glob pattern to validate + + Returns: + Error message if validation fails, None if valid + """ + if not pattern: + return "Pattern cannot be empty" + + # Check for potentially dangerous patterns + if pattern.startswith('/') or pattern.startswith('\\'): + return "Pattern cannot start with path separator" + + # Test if the pattern is valid by trying to compile it + try: + # This will raise an exception if the pattern is malformed + fnmatch.translate(pattern) + except (ValueError, TypeError) as e: + return f"Invalid glob pattern: {str(e)}" + + return None + + @staticmethod + def validate_search_pattern(pattern: str, regex: bool = False) -> Optional[str]: + """ + Validate a search pattern for code searching. + + Args: + pattern: The search pattern to validate + regex: Whether the pattern is a regex pattern + + Returns: + Error message if validation fails, None if valid + """ + if not pattern: + return "Search pattern cannot be empty" + + if regex: + # Basic regex validation - check for potentially dangerous patterns + try: + re.compile(pattern) + except re.error as e: + return ( + f"Invalid regex pattern: {str(e)}. " + "If you intended a literal search, pass regex=False." + ) + + # Check for potentially expensive regex patterns (basic ReDoS protection) + dangerous_patterns = [ + r'\(\?\=.*\)\+', # Positive lookahead with quantifier + r'\(\?\!.*\)\+', # Negative lookahead with quantifier + r'\(\?\<\=.*\)\+', # Positive lookbehind with quantifier + r'\(\?\<\!.*\)\+', # Negative lookbehind with quantifier + ] + + for dangerous in dangerous_patterns: + if re.search(dangerous, pattern): + return "Potentially dangerous regex pattern detected" + + return None + + @staticmethod + def validate_pagination(start_index: int, max_results: Optional[int]) -> Optional[str]: + """ + Validate pagination parameters for search queries. + + Args: + start_index: The index of the first result to include. + max_results: The maximum number of results to return. + + Returns: + Error message if validation fails, None if valid. + """ + if not isinstance(start_index, int): + return "start_index must be an integer" + + if start_index < 0: + return "start_index cannot be negative" + + if max_results is None: + return None + + if not isinstance(max_results, int): + return "max_results must be an integer when provided" + + if max_results <= 0: + return "max_results must be greater than zero when provided" + + return None + + @staticmethod + def validate_file_extensions(extensions: List[str]) -> Optional[str]: + """ + Validate a list of file extensions. + + Args: + extensions: List of file extensions to validate + + Returns: + Error message if validation fails, None if valid + """ + if not extensions: + return "Extensions list cannot be empty" + + for ext in extensions: + if not isinstance(ext, str): + return "All extensions must be strings" + + if not ext.startswith('.'): + return f"Extension '{ext}' must start with a dot" + + if len(ext) < 2: + return f"Extension '{ext}' is too short" + + return None + + @staticmethod + def sanitize_file_path(file_path: str) -> str: + """ + Sanitize a file path by normalizing separators and removing dangerous elements. + + Args: + file_path: The file path to sanitize + + Returns: + Sanitized file path + """ + if not file_path: + return "" + + # Normalize path separators and structure + sanitized = normalize_file_path(file_path) + + # Remove any leading slashes to ensure relative path + sanitized = sanitized.lstrip('/') + + return sanitized diff --git a/reference/code-index-mcp-master/test/README.md b/reference/code-index-mcp-master/test/README.md new file mode 100644 index 00000000..5796cb3c --- /dev/null +++ b/reference/code-index-mcp-master/test/README.md @@ -0,0 +1,247 @@ +# Test Projects for Code Index MCP + +This directory contains comprehensive test projects designed to validate and demonstrate the capabilities of the Code Index MCP server. Each project represents a realistic, enterprise-level codebase that showcases different programming languages, frameworks, and architectural patterns. + +## Project Structure + +``` +test/ +├── sample-projects/ +│ ├── python/ +│ │ └── user_management/ # Python user management system +│ ├── java/ +│ │ └── user-management/ # Java Spring Boot user management +│ ├── go/ +│ │ └── user-management/ # Go Gin user management API +│ ├── javascript/ +│ │ └── user-management/ # Node.js Express user management +│ ├── typescript/ +│ │ └── user-management/ # TypeScript Express user management +│ └── objective-c/ # Objective-C test files +└── README.md # This file +``` + +## Sample Projects Overview + +Each sample project implements a comprehensive user management system with the following core features: + +### Common Features Across All Projects +- **User Registration & Authentication**: Secure user registration with password hashing +- **Role-Based Access Control (RBAC)**: Admin, User, and Guest roles with permissions +- **CRUD Operations**: Complete Create, Read, Update, Delete functionality +- **Search & Filtering**: Full-text search and role/status-based filtering +- **Pagination**: Efficient pagination for large datasets +- **Input Validation**: Comprehensive validation and sanitization +- **Error Handling**: Structured error handling with custom error classes +- **Logging**: Structured logging for debugging and monitoring +- **Security**: Password hashing, rate limiting, and security headers +- **Data Export**: User data export functionality +- **Statistics**: User analytics and statistics + +### Language-Specific Implementation Details + +#### Python Project (`python/user_management/`) +- **Framework**: Flask-based web application +- **Database**: SQLAlchemy ORM with SQLite +- **Authentication**: JWT tokens with BCrypt password hashing +- **Structure**: Clean package structure with models, services, and utilities +- **Features**: CLI interface, comprehensive validation, and export functionality + +**Key Files:** +- `models/person.py` - Base Person model +- `models/user.py` - User model with authentication +- `services/user_manager.py` - Business logic layer +- `services/auth_service.py` - Authentication service +- `utils/` - Validation, exceptions, and helper utilities +- `cli.py` - Command-line interface + +#### Java Project (`java/user-management/`) +- **Framework**: Spring Boot with Spring Data JPA +- **Database**: H2 in-memory database with JPA +- **Authentication**: JWT tokens with BCrypt +- **Structure**: Maven project with standard Java package structure +- **Features**: REST API, validation annotations, and comprehensive testing + +**Key Files:** +- `model/User.java` - JPA entity with validation +- `service/UserService.java` - Business logic service +- `controller/UserController.java` - REST API endpoints +- `util/` - Validation, exceptions, and utilities +- `Application.java` - Spring Boot application entry point + +#### Go Project (`go/user-management/`) +- **Framework**: Gin web framework with GORM +- **Database**: SQLite with GORM ORM +- **Authentication**: JWT tokens with BCrypt +- **Structure**: Clean Go module structure with internal packages +- **Features**: High-performance API, middleware, and concurrent processing + +**Key Files:** +- `internal/models/user.go` - User model with GORM +- `internal/services/user_service.go` - Business logic +- `pkg/api/handlers/user_handler.go` - HTTP handlers +- `pkg/middleware/` - Authentication and validation middleware +- `cmd/server/main.go` - Application entry point + +#### JavaScript Project (`javascript/user-management/`) +- **Framework**: Express.js with Mongoose +- **Database**: MongoDB with Mongoose ODM +- **Authentication**: JWT tokens with BCrypt +- **Structure**: Modern Node.js project with ES6+ features +- **Features**: Async/await, middleware, and comprehensive error handling + +**Key Files:** +- `src/models/User.js` - Mongoose model with validation +- `src/services/UserService.js` - Business logic service +- `src/routes/userRoutes.js` - Express routes +- `src/middleware/` - Authentication and validation middleware +- `src/server.js` - Express application setup + +#### TypeScript Project (`typescript/user-management/`) +- **Framework**: Express.js with Mongoose (TypeScript) +- **Database**: MongoDB with Mongoose ODM +- **Authentication**: JWT tokens with BCrypt +- **Structure**: Type-safe Node.js project with comprehensive interfaces +- **Features**: Full type safety, interfaces, and advanced TypeScript features + +**Key Files:** +- `src/types/User.ts` - TypeScript interfaces and types +- `src/models/User.ts` - Mongoose model with TypeScript +- `src/services/UserService.ts` - Typed business logic service +- `src/routes/userRoutes.ts` - Typed Express routes +- `src/server.ts` - TypeScript Express application + +#### Objective-C Project (`objective-c/`) +- **Framework**: Foundation classes +- **Features**: Classes, properties, methods, protocols +- **Structure**: Traditional .h/.m file structure + +**Key Files:** +- `Person.h/.m` - Person class with properties +- `UserManager.h/.m` - User management functionality +- `main.m` - Application entry point + +## Testing the Code Index MCP + +These projects are designed to test various aspects of the Code Index MCP: + +### File Analysis Capabilities +- **Language Detection**: Automatic detection of programming languages +- **Syntax Parsing**: Parsing of different syntax structures +- **Import/Dependency Analysis**: Understanding of module dependencies +- **Code Structure**: Recognition of classes, functions, and interfaces + +### Search and Navigation +- **Symbol Search**: Finding functions, classes, and variables +- **Cross-Reference**: Finding usage of symbols across files +- **Fuzzy Search**: Approximate matching for typos and partial queries +- **Pattern Matching**: Regular expression and pattern-based searches + +### Code Intelligence +- **Function Signatures**: Understanding of function parameters and return types +- **Variable Types**: Type inference and tracking +- **Scope Analysis**: Understanding of variable and function scope +- **Documentation**: Parsing of comments and documentation + +### Performance Testing +- **Large Codebases**: Testing with realistic project sizes +- **Complex Structures**: Nested packages and deep directory structures +- **Multiple File Types**: Mixed file types within projects +- **Concurrent Access**: Multiple simultaneous search operations + +## Running the Projects + +Each project includes comprehensive setup instructions in its respective README.md file. General steps: + +1. Navigate to the project directory +2. Install dependencies using the appropriate package manager +3. Set up environment variables (see .env.example files) +4. Run the application using the provided scripts +5. Test the API endpoints using the provided examples + +### Quick Start Examples + +```bash +# Python project +cd test/sample-projects/python/user_management +pip install -r requirements.txt +python cli.py + +# Java project +cd test/sample-projects/java/user-management +mvn spring-boot:run + +# Go project +cd test/sample-projects/go/user-management +go run cmd/server/main.go + +# JavaScript project +cd test/sample-projects/javascript/user-management +npm install +npm run dev + +# TypeScript project +cd test/sample-projects/typescript/user-management +npm install +npm run dev +``` + +## MCP Server Testing + +To test the Code Index MCP server with these projects: + +1. **Set Project Path**: Use the `set_project_path` tool to point to a project directory +2. **Index Files**: The server will automatically index all files in the project +3. **Search Testing**: Test various search queries and patterns +4. **Analysis Testing**: Use the analysis tools to examine code structure +5. **Performance Testing**: Measure response times and resource usage + +### Example MCP Commands + +```bash +# Set project path +set_project_path /path/to/test/sample-projects/python/user_management + +# Search for user-related functions +search_code_advanced "def create_user" --file-pattern "*.py" + +# Find all authentication-related code +search_code_advanced "auth" --fuzzy true + +# Get file summary +get_file_summary models/user.py + +# Find TypeScript interfaces +search_code_advanced "interface.*User" --regex true --file-pattern "*.ts" +``` + +## Contributing + +When adding new test projects: + +1. Follow the established patterns and structure +2. Implement all core features consistently +3. Include comprehensive documentation +4. Add appropriate test cases +5. Update this README with project details + +## Security Considerations + +All test projects include: +- Secure password hashing (BCrypt) +- Input validation and sanitization +- Rate limiting and security headers +- JWT token-based authentication +- Environment variable configuration +- Proper error handling without information disclosure + +## Future Enhancements + +Potential additions to the test suite: +- **Rust Project**: Systems programming language example +- **C++ Project**: Complex C++ codebase with templates +- **C# Project**: .NET Core application +- **PHP Project**: Laravel-based web application +- **Ruby Project**: Rails application +- **Swift Project**: iOS application structure +- **Kotlin Project**: Android/JVM application \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/go/user-management/README.md b/reference/code-index-mcp-master/test/sample-projects/go/user-management/README.md new file mode 100644 index 00000000..4534cba3 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/go/user-management/README.md @@ -0,0 +1,324 @@ +# User Management System (Go) + +A comprehensive user management system built in Go for testing Code Index MCP's analysis capabilities. + +## Features + +- **User Management**: Create, update, delete, and search users +- **REST API**: Full HTTP API with JSON responses +- **Authentication**: BCrypt password hashing and JWT tokens +- **Authorization**: Role-based access control (Admin, User, Guest) +- **Database**: SQLite with GORM ORM +- **Pagination**: Efficient pagination for large datasets +- **Search**: Full-text search across users +- **Export**: JSON export functionality +- **Logging**: Structured logging with middleware +- **CORS**: Cross-origin resource sharing support + +## Project Structure + +``` +user-management/ +├── cmd/ +│ ├── server/ +│ │ └── main.go # HTTP server entry point +│ └── cli/ +│ └── main.go # CLI application +├── internal/ +│ ├── models/ +│ │ └── user.go # User model and types +│ ├── services/ +│ │ └── user_service.go # Business logic +│ └── utils/ +│ └── types.go # Utility types and helpers +├── pkg/ +│ └── api/ +│ └── user_handler.go # HTTP handlers +├── go.mod # Go module file +├── go.sum # Go dependencies +└── README.md # This file +``` + +## Technologies Used + +- **Go 1.21**: Modern Go with generics and latest features +- **Gin**: HTTP web framework +- **GORM**: ORM for database operations +- **SQLite**: Embedded database +- **UUID**: Unique identifiers +- **BCrypt**: Password hashing +- **JWT**: JSON Web Tokens (planned) +- **Viper**: Configuration management +- **Cobra**: CLI framework + +## Build and Run + +### Prerequisites + +- Go 1.21 or higher + +### Install Dependencies + +```bash +go mod tidy +``` + +### Run HTTP Server + +```bash +go run cmd/server/main.go +``` + +The server will start on `http://localhost:8080` + +### Run CLI + +```bash +go run cmd/cli/main.go +``` + +### Build + +```bash +# Build server +go build -o bin/server cmd/server/main.go + +# Build CLI +go build -o bin/cli cmd/cli/main.go +``` + +## API Endpoints + +### Users + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/v1/users` | Create a new user | +| `GET` | `/api/v1/users` | Get all users (paginated) | +| `GET` | `/api/v1/users/:id` | Get user by ID | +| `PUT` | `/api/v1/users/:id` | Update user | +| `DELETE` | `/api/v1/users/:id` | Delete user | +| `GET` | `/api/v1/users/search` | Search users | +| `GET` | `/api/v1/users/stats` | Get user statistics | +| `GET` | `/api/v1/users/export` | Export users | + +### Authentication + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/v1/auth/login` | User login | +| `POST` | `/api/v1/auth/logout` | User logout | +| `POST` | `/api/v1/auth/change-password` | Change password | + +### Admin + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/v1/admin/users/:id/reset-password` | Reset user password | +| `POST` | `/api/v1/admin/users/:id/permissions` | Add permission | +| `DELETE` | `/api/v1/admin/users/:id/permissions` | Remove permission | + +## Usage Examples + +### Create User + +```bash +curl -X POST http://localhost:8080/api/v1/users \ + -H "Content-Type: application/json" \ + -d '{ + "username": "johndoe", + "email": "john@example.com", + "name": "John Doe", + "age": 30, + "password": "password123" + }' +``` + +### Get Users + +```bash +curl http://localhost:8080/api/v1/users?page=1&page_size=10 +``` + +### Search Users + +```bash +curl http://localhost:8080/api/v1/users/search?q=john&page=1&page_size=10 +``` + +### Login + +```bash +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "admin", + "password": "admin123" + }' +``` + +### Get Statistics + +```bash +curl http://localhost:8080/api/v1/users/stats +``` + +## Programmatic Usage + +```go +package main + +import ( + "github.com/example/user-management/internal/models" + "github.com/example/user-management/internal/services" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func main() { + // Initialize database + db, err := gorm.Open(sqlite.Open("users.db"), &gorm.Config{}) + if err != nil { + panic(err) + } + + // Auto migrate + db.AutoMigrate(&models.User{}) + + // Initialize service + userService := services.NewUserService(db) + + // Create user + req := &models.UserRequest{ + Username: "alice", + Email: "alice@example.com", + Name: "Alice Smith", + Age: 25, + Password: "password123", + Role: models.RoleUser, + } + + user, err := userService.CreateUser(req) + if err != nil { + panic(err) + } + + // Authenticate user + authUser, err := userService.AuthenticateUser("alice", "password123") + if err != nil { + panic(err) + } + + // Get statistics + stats, err := userService.GetUserStats() + if err != nil { + panic(err) + } +} +``` + +## Testing Features + +This project tests the following Go language features: + +### Core Language Features +- **Structs and Methods**: User model with associated methods +- **Interfaces**: Service and handler interfaces +- **Pointers**: Efficient memory management +- **Error Handling**: Comprehensive error handling patterns +- **Packages**: Modular code organization +- **Imports**: Internal and external package imports + +### Modern Go Features +- **Generics**: Type-safe collections (Go 1.18+) +- **Modules**: Dependency management with go.mod +- **Context**: Request context handling +- **Channels**: Concurrent programming (in background tasks) +- **Goroutines**: Concurrent execution +- **JSON Tags**: Struct field mapping + +### Advanced Features +- **Reflection**: GORM model reflection +- **Build Tags**: Conditional compilation +- **Embedding**: Struct embedding for composition +- **Type Assertions**: Interface type checking +- **Panic/Recover**: Error recovery mechanisms + +### Framework Integration +- **Gin**: HTTP router and middleware +- **GORM**: ORM with hooks and associations +- **UUID**: Unique identifier generation +- **BCrypt**: Cryptographic hashing +- **SQLite**: Embedded database + +### Design Patterns +- **Repository Pattern**: Data access layer +- **Service Layer**: Business logic separation +- **Dependency Injection**: Service composition +- **Middleware Pattern**: HTTP request processing +- **Factory Pattern**: Service creation + +## Dependencies + +### Core Dependencies +- **gin-gonic/gin**: Web framework +- **gorm.io/gorm**: ORM +- **gorm.io/driver/sqlite**: SQLite driver +- **google/uuid**: UUID generation +- **golang.org/x/crypto**: Cryptographic functions + +### CLI Dependencies +- **spf13/cobra**: CLI framework +- **spf13/viper**: Configuration management + +### Development Dependencies +- **testify**: Testing framework +- **mockery**: Mock generation + +## Configuration + +The application can be configured using environment variables or a configuration file: + +```yaml +database: + driver: sqlite + database: users.db + +server: + port: 8080 + host: localhost + +jwt: + secret_key: your-secret-key + expiration_hours: 24 +``` + +## Development + +### Run Tests + +```bash +go test ./... +``` + +### Generate Mocks + +```bash +mockery --all +``` + +### Format Code + +```bash +gofmt -w . +``` + +### Lint Code + +```bash +golangci-lint run +``` + +## License + +MIT License - This is a sample project for testing purposes. \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/go/user-management/cmd/server/main.go b/reference/code-index-mcp-master/test/sample-projects/go/user-management/cmd/server/main.go new file mode 100644 index 00000000..690614a9 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/go/user-management/cmd/server/main.go @@ -0,0 +1,294 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "time" + + "github.com/example/user-management/internal/models" + "github.com/example/user-management/internal/services" + "github.com/example/user-management/internal/utils" + "github.com/example/user-management/pkg/api" + "github.com/gin-gonic/gin" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func main() { + // Initialize database + db, err := initDatabase() + if err != nil { + log.Fatal("Failed to initialize database:", err) + } + + // Initialize services + userService := services.NewUserService(db) + + // Initialize API handlers + userHandler := api.NewUserHandler(userService) + + // Setup routes + router := setupRoutes(userHandler) + + // Create sample data + createSampleData(userService) + + // Start server + log.Println("Starting server on :8080") + if err := router.Run(":8080"); err != nil { + log.Fatal("Failed to start server:", err) + } +} + +func initDatabase() (*gorm.DB, error) { + db, err := gorm.Open(sqlite.Open("users.db"), &gorm.Config{}) + if err != nil { + return nil, err + } + + // Auto migrate + if err := db.AutoMigrate(&models.User{}); err != nil { + return nil, err + } + + return db, nil +} + +func setupRoutes(userHandler *api.UserHandler) *gin.Engine { + router := gin.Default() + + // Middleware + router.Use(corsMiddleware()) + router.Use(loggingMiddleware()) + + // Health check + router.GET("/health", healthCheck) + + // API routes + v1 := router.Group("/api/v1") + { + users := v1.Group("/users") + { + users.POST("", userHandler.CreateUser) + users.GET("", userHandler.GetUsers) + users.GET("/:id", userHandler.GetUser) + users.PUT("/:id", userHandler.UpdateUser) + users.DELETE("/:id", userHandler.DeleteUser) + users.GET("/search", userHandler.SearchUsers) + users.GET("/stats", userHandler.GetUserStats) + users.GET("/export", userHandler.ExportUsers) + } + + auth := v1.Group("/auth") + { + auth.POST("/login", userHandler.Login) + auth.POST("/logout", userHandler.Logout) + auth.POST("/change-password", userHandler.ChangePassword) + } + + admin := v1.Group("/admin") + { + admin.POST("/users/:id/reset-password", userHandler.ResetPassword) + admin.POST("/users/:id/permissions", userHandler.AddPermission) + admin.DELETE("/users/:id/permissions", userHandler.RemovePermission) + } + } + + return router +} + +func healthCheck(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "timestamp": time.Now().UTC(), + "version": "1.0.0", + }) +} + +func corsMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusOK) + return + } + + c.Next() + } +} + +func loggingMiddleware() gin.HandlerFunc { + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", + param.ClientIP, + param.TimeStamp.Format(time.RFC1123), + param.Method, + param.Path, + param.Request.Proto, + param.StatusCode, + param.Latency, + param.Request.UserAgent(), + param.ErrorMessage, + ) + }) +} + +func createSampleData(userService *services.UserService) { + // Check if admin user already exists + if _, err := userService.GetUserByUsername("admin"); err == nil { + return // Admin user already exists + } + + // Create admin user + adminReq := &models.UserRequest{ + Username: "admin", + Email: "admin@example.com", + Name: "System Administrator", + Age: 30, + Password: "admin123", + Role: models.RoleAdmin, + } + + admin, err := userService.CreateUser(adminReq) + if err != nil { + log.Printf("Failed to create admin user: %v", err) + return + } + + // Add admin permissions + permissions := []string{ + "user_management", + "system_admin", + "user_read", + "user_write", + "user_delete", + } + + for _, perm := range permissions { + if err := userService.AddPermission(admin.ID, perm); err != nil { + log.Printf("Failed to add permission %s to admin: %v", perm, err) + } + } + + // Create sample users + sampleUsers := []*models.UserRequest{ + { + Username: "john_doe", + Email: "john@example.com", + Name: "John Doe", + Age: 25, + Password: "password123", + Role: models.RoleUser, + }, + { + Username: "jane_smith", + Email: "jane@example.com", + Name: "Jane Smith", + Age: 28, + Password: "password123", + Role: models.RoleUser, + }, + { + Username: "guest_user", + Email: "guest@example.com", + Name: "Guest User", + Age: 22, + Password: "password123", + Role: models.RoleGuest, + }, + } + + for _, userReq := range sampleUsers { + if _, err := userService.CreateUser(userReq); err != nil { + log.Printf("Failed to create user %s: %v", userReq.Username, err) + } + } + + log.Println("Sample data created successfully") +} + +// Helper functions for demo +func printUserStats(userService *services.UserService) { + stats, err := userService.GetUserStats() + if err != nil { + log.Printf("Failed to get user stats: %v", err) + return + } + + log.Printf("User Statistics:") + log.Printf(" Total: %d", stats.Total) + log.Printf(" Active: %d", stats.Active) + log.Printf(" Admin: %d", stats.Admin) + log.Printf(" User: %d", stats.User) + log.Printf(" Guest: %d", stats.Guest) + log.Printf(" With Email: %d", stats.WithEmail) +} + +func demonstrateUserOperations(userService *services.UserService) { + log.Println("\n=== User Management Demo ===") + + // Get all users + users, total, err := userService.GetAllUsers(1, 10) + if err != nil { + log.Printf("Failed to get users: %v", err) + return + } + + log.Printf("Found %d users (total: %d):", len(users), total) + for _, user := range users { + log.Printf(" - %s (%s) - %s [%s]", + user.Username, user.Name, user.Role, user.Status) + } + + // Test authentication + log.Println("\n=== Authentication Test ===") + user, err := userService.AuthenticateUser("admin", "admin123") + if err != nil { + log.Printf("Authentication failed: %v", err) + } else { + log.Printf("Authentication successful for: %s", user.Username) + log.Printf("Last login: %v", user.LastLogin) + } + + // Test search + log.Println("\n=== Search Test ===") + searchResults, _, err := userService.SearchUsers("john", 1, 10) + if err != nil { + log.Printf("Search failed: %v", err) + } else { + log.Printf("Search results for 'john': %d users", len(searchResults)) + for _, user := range searchResults { + log.Printf(" - %s (%s)", user.Username, user.Name) + } + } + + // Print stats + log.Println("\n=== Statistics ===") + printUserStats(userService) +} + +// Run demo if not in server mode +func runDemo() { + log.Println("Running User Management Demo...") + + // Initialize database + db, err := initDatabase() + if err != nil { + log.Fatal("Failed to initialize database:", err) + } + + // Initialize services + userService := services.NewUserService(db) + + // Create sample data + createSampleData(userService) + + // Demonstrate operations + demonstrateUserOperations(userService) + + log.Println("\nDemo completed!") +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/go/user-management/go.mod b/reference/code-index-mcp-master/test/sample-projects/go/user-management/go.mod new file mode 100644 index 00000000..ae1357e4 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/go/user-management/go.mod @@ -0,0 +1,53 @@ +module github.com/example/user-management + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/uuid v1.3.0 + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.16.0 + golang.org/x/crypto v0.11.0 + gorm.io/driver/sqlite v1.5.2 + gorm.io/gorm v1.25.2 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/models/user.go b/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/models/user.go new file mode 100644 index 00000000..465abf0d --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/models/user.go @@ -0,0 +1,310 @@ +package models + +import ( + "encoding/json" + "errors" + "time" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// UserRole represents the role of a user +type UserRole string + +const ( + RoleAdmin UserRole = "admin" + RoleUser UserRole = "user" + RoleGuest UserRole = "guest" +) + +// UserStatus represents the status of a user +type UserStatus string + +const ( + StatusActive UserStatus = "active" + StatusInactive UserStatus = "inactive" + StatusSuspended UserStatus = "suspended" + StatusDeleted UserStatus = "deleted" +) + +// User represents a user in the system +type User struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key"` + Username string `json:"username" gorm:"uniqueIndex;not null"` + Email string `json:"email" gorm:"uniqueIndex"` + Name string `json:"name" gorm:"not null"` + Age int `json:"age"` + PasswordHash string `json:"-" gorm:"not null"` + Role UserRole `json:"role" gorm:"default:user"` + Status UserStatus `json:"status" gorm:"default:active"` + LastLogin *time.Time `json:"last_login"` + LoginAttempts int `json:"login_attempts" gorm:"default:0"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // Permissions is a JSON field containing user permissions + Permissions []string `json:"permissions" gorm:"type:json"` + + // Metadata for additional user information + Metadata map[string]interface{} `json:"metadata" gorm:"type:json"` +} + +// UserRequest represents a request to create or update a user +type UserRequest struct { + Username string `json:"username" binding:"required,min=3,max=20"` + Email string `json:"email" binding:"omitempty,email"` + Name string `json:"name" binding:"required,min=1,max=100"` + Age int `json:"age" binding:"min=0,max=150"` + Password string `json:"password" binding:"required,min=8"` + Role UserRole `json:"role" binding:"omitempty,oneof=admin user guest"` + Metadata map[string]interface{} `json:"metadata"` +} + +// UserResponse represents a user response (without sensitive data) +type UserResponse struct { + ID uuid.UUID `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + Age int `json:"age"` + Role UserRole `json:"role"` + Status UserStatus `json:"status"` + LastLogin *time.Time `json:"last_login"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Permissions []string `json:"permissions"` + Metadata map[string]interface{} `json:"metadata"` +} + +// BeforeCreate is a GORM hook that runs before creating a user +func (u *User) BeforeCreate(tx *gorm.DB) error { + if u.ID == uuid.Nil { + u.ID = uuid.New() + } + + if u.Permissions == nil { + u.Permissions = []string{} + } + + if u.Metadata == nil { + u.Metadata = make(map[string]interface{}) + } + + return nil +} + +// SetPassword hashes and sets the user's password +func (u *User) SetPassword(password string) error { + if len(password) < 8 { + return errors.New("password must be at least 8 characters long") + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err + } + + u.PasswordHash = string(hash) + return nil +} + +// VerifyPassword checks if the provided password matches the user's password +func (u *User) VerifyPassword(password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) + return err == nil +} + +// HasPermission checks if the user has a specific permission +func (u *User) HasPermission(permission string) bool { + for _, p := range u.Permissions { + if p == permission { + return true + } + } + return false +} + +// AddPermission adds a permission to the user +func (u *User) AddPermission(permission string) { + if !u.HasPermission(permission) { + u.Permissions = append(u.Permissions, permission) + } +} + +// RemovePermission removes a permission from the user +func (u *User) RemovePermission(permission string) { + for i, p := range u.Permissions { + if p == permission { + u.Permissions = append(u.Permissions[:i], u.Permissions[i+1:]...) + break + } + } +} + +// IsActive checks if the user is active +func (u *User) IsActive() bool { + return u.Status == StatusActive +} + +// IsAdmin checks if the user is an admin +func (u *User) IsAdmin() bool { + return u.Role == RoleAdmin +} + +// IsLocked checks if the user is locked due to too many failed login attempts +func (u *User) IsLocked() bool { + return u.LoginAttempts >= 5 || u.Status == StatusSuspended +} + +// Login records a successful login +func (u *User) Login() error { + if !u.IsActive() { + return errors.New("user is not active") + } + + if u.IsLocked() { + return errors.New("user is locked") + } + + now := time.Now() + u.LastLogin = &now + u.LoginAttempts = 0 + + return nil +} + +// FailedLoginAttempt records a failed login attempt +func (u *User) FailedLoginAttempt() { + u.LoginAttempts++ + if u.LoginAttempts >= 5 { + u.Status = StatusSuspended + } +} + +// ResetLoginAttempts resets the login attempts counter +func (u *User) ResetLoginAttempts() { + u.LoginAttempts = 0 +} + +// Activate activates the user account +func (u *User) Activate() { + u.Status = StatusActive + u.LoginAttempts = 0 +} + +// Deactivate deactivates the user account +func (u *User) Deactivate() { + u.Status = StatusInactive +} + +// Suspend suspends the user account +func (u *User) Suspend() { + u.Status = StatusSuspended +} + +// Delete marks the user as deleted +func (u *User) Delete() { + u.Status = StatusDeleted +} + +// ToResponse converts a User to a UserResponse +func (u *User) ToResponse() *UserResponse { + return &UserResponse{ + ID: u.ID, + Username: u.Username, + Email: u.Email, + Name: u.Name, + Age: u.Age, + Role: u.Role, + Status: u.Status, + LastLogin: u.LastLogin, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + Permissions: u.Permissions, + Metadata: u.Metadata, + } +} + +// FromRequest creates a User from a UserRequest +func (u *User) FromRequest(req *UserRequest) error { + u.Username = req.Username + u.Email = req.Email + u.Name = req.Name + u.Age = req.Age + u.Role = req.Role + u.Metadata = req.Metadata + + if req.Password != "" { + return u.SetPassword(req.Password) + } + + return nil +} + +// MarshalJSON customizes JSON marshaling for User +func (u *User) MarshalJSON() ([]byte, error) { + return json.Marshal(u.ToResponse()) +} + +// Validate validates the user model +func (u *User) Validate() error { + if len(u.Username) < 3 || len(u.Username) > 20 { + return errors.New("username must be between 3 and 20 characters") + } + + if len(u.Name) == 0 || len(u.Name) > 100 { + return errors.New("name must be between 1 and 100 characters") + } + + if u.Age < 0 || u.Age > 150 { + return errors.New("age must be between 0 and 150") + } + + if u.Role != RoleAdmin && u.Role != RoleUser && u.Role != RoleGuest { + return errors.New("invalid role") + } + + if u.Status != StatusActive && u.Status != StatusInactive && + u.Status != StatusSuspended && u.Status != StatusDeleted { + return errors.New("invalid status") + } + + return nil +} + +// TableName returns the table name for GORM +func (u *User) TableName() string { + return "users" +} + +// GetMetadata gets a metadata value by key +func (u *User) GetMetadata(key string) (interface{}, bool) { + if u.Metadata == nil { + return nil, false + } + value, exists := u.Metadata[key] + return value, exists +} + +// SetMetadata sets a metadata value +func (u *User) SetMetadata(key string, value interface{}) { + if u.Metadata == nil { + u.Metadata = make(map[string]interface{}) + } + u.Metadata[key] = value +} + +// RemoveMetadata removes a metadata key +func (u *User) RemoveMetadata(key string) { + if u.Metadata != nil { + delete(u.Metadata, key) + } +} + +// String returns a string representation of the user +func (u *User) String() string { + return u.Username + " (" + u.Name + ")" +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/services/user_service.go b/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/services/user_service.go new file mode 100644 index 00000000..aeb31069 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/services/user_service.go @@ -0,0 +1,419 @@ +package services + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/example/user-management/internal/models" + "github.com/example/user-management/internal/utils" + "github.com/google/uuid" + "gorm.io/gorm" +) + +// UserService handles user-related business logic +type UserService struct { + db *gorm.DB +} + +// NewUserService creates a new user service +func NewUserService(db *gorm.DB) *UserService { + return &UserService{db: db} +} + +// CreateUser creates a new user +func (s *UserService) CreateUser(req *models.UserRequest) (*models.User, error) { + // Check if username already exists + var existingUser models.User + if err := s.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil { + return nil, errors.New("username already exists") + } + + // Check if email already exists (if provided) + if req.Email != "" { + if err := s.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil { + return nil, errors.New("email already exists") + } + } + + // Create new user + user := &models.User{ + Role: models.RoleUser, + Status: models.StatusActive, + } + + if err := user.FromRequest(req); err != nil { + return nil, fmt.Errorf("failed to create user from request: %w", err) + } + + if err := user.Validate(); err != nil { + return nil, fmt.Errorf("user validation failed: %w", err) + } + + if err := s.db.Create(user).Error; err != nil { + return nil, fmt.Errorf("failed to create user: %w", err) + } + + return user, nil +} + +// GetUserByID retrieves a user by ID +func (s *UserService) GetUserByID(id uuid.UUID) (*models.User, error) { + var user models.User + if err := s.db.First(&user, "id = ?", id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("user not found") + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + return &user, nil +} + +// GetUserByUsername retrieves a user by username +func (s *UserService) GetUserByUsername(username string) (*models.User, error) { + var user models.User + if err := s.db.Where("username = ?", username).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("user not found") + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + return &user, nil +} + +// GetUserByEmail retrieves a user by email +func (s *UserService) GetUserByEmail(email string) (*models.User, error) { + var user models.User + if err := s.db.Where("email = ?", email).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("user not found") + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + return &user, nil +} + +// UpdateUser updates an existing user +func (s *UserService) UpdateUser(id uuid.UUID, updates map[string]interface{}) (*models.User, error) { + user, err := s.GetUserByID(id) + if err != nil { + return nil, err + } + + // Apply updates + for key, value := range updates { + switch key { + case "name": + if name, ok := value.(string); ok { + user.Name = name + } + case "age": + if age, ok := value.(int); ok { + user.Age = age + } + case "email": + if email, ok := value.(string); ok { + user.Email = email + } + case "role": + if role, ok := value.(models.UserRole); ok { + user.Role = role + } + case "status": + if status, ok := value.(models.UserStatus); ok { + user.Status = status + } + case "metadata": + if metadata, ok := value.(map[string]interface{}); ok { + user.Metadata = metadata + } + } + } + + if err := user.Validate(); err != nil { + return nil, fmt.Errorf("user validation failed: %w", err) + } + + if err := s.db.Save(user).Error; err != nil { + return nil, fmt.Errorf("failed to update user: %w", err) + } + + return user, nil +} + +// DeleteUser soft deletes a user +func (s *UserService) DeleteUser(id uuid.UUID) error { + user, err := s.GetUserByID(id) + if err != nil { + return err + } + + user.Delete() + + if err := s.db.Save(user).Error; err != nil { + return fmt.Errorf("failed to delete user: %w", err) + } + + return nil +} + +// HardDeleteUser permanently deletes a user +func (s *UserService) HardDeleteUser(id uuid.UUID) error { + if err := s.db.Unscoped().Delete(&models.User{}, id).Error; err != nil { + return fmt.Errorf("failed to hard delete user: %w", err) + } + return nil +} + +// GetAllUsers retrieves all users with pagination +func (s *UserService) GetAllUsers(page, pageSize int) ([]*models.User, int64, error) { + var users []*models.User + var total int64 + + // Count total users + if err := s.db.Model(&models.User{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count users: %w", err) + } + + // Get users with pagination + offset := (page - 1) * pageSize + if err := s.db.Limit(pageSize).Offset(offset).Find(&users).Error; err != nil { + return nil, 0, fmt.Errorf("failed to get users: %w", err) + } + + return users, total, nil +} + +// GetActiveUsers retrieves all active users +func (s *UserService) GetActiveUsers() ([]*models.User, error) { + var users []*models.User + if err := s.db.Where("status = ?", models.StatusActive).Find(&users).Error; err != nil { + return nil, fmt.Errorf("failed to get active users: %w", err) + } + return users, nil +} + +// GetUsersByRole retrieves users by role +func (s *UserService) GetUsersByRole(role models.UserRole) ([]*models.User, error) { + var users []*models.User + if err := s.db.Where("role = ?", role).Find(&users).Error; err != nil { + return nil, fmt.Errorf("failed to get users by role: %w", err) + } + return users, nil +} + +// SearchUsers searches for users by name or username +func (s *UserService) SearchUsers(query string, page, pageSize int) ([]*models.User, int64, error) { + var users []*models.User + var total int64 + + searchQuery := "%" + strings.ToLower(query) + "%" + + // Count total matching users + if err := s.db.Model(&models.User{}).Where( + "LOWER(name) LIKE ? OR LOWER(username) LIKE ? OR LOWER(email) LIKE ?", + searchQuery, searchQuery, searchQuery, + ).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count search results: %w", err) + } + + // Get matching users with pagination + offset := (page - 1) * pageSize + if err := s.db.Where( + "LOWER(name) LIKE ? OR LOWER(username) LIKE ? OR LOWER(email) LIKE ?", + searchQuery, searchQuery, searchQuery, + ).Limit(pageSize).Offset(offset).Find(&users).Error; err != nil { + return nil, 0, fmt.Errorf("failed to search users: %w", err) + } + + return users, total, nil +} + +// GetUserStats returns user statistics +func (s *UserService) GetUserStats() (*utils.UserStats, error) { + var stats utils.UserStats + + // Total users + if err := s.db.Model(&models.User{}).Count(&stats.Total).Error; err != nil { + return nil, fmt.Errorf("failed to count total users: %w", err) + } + + // Active users + if err := s.db.Model(&models.User{}).Where("status = ?", models.StatusActive).Count(&stats.Active).Error; err != nil { + return nil, fmt.Errorf("failed to count active users: %w", err) + } + + // Admin users + if err := s.db.Model(&models.User{}).Where("role = ?", models.RoleAdmin).Count(&stats.Admin).Error; err != nil { + return nil, fmt.Errorf("failed to count admin users: %w", err) + } + + // Regular users + if err := s.db.Model(&models.User{}).Where("role = ?", models.RoleUser).Count(&stats.User).Error; err != nil { + return nil, fmt.Errorf("failed to count regular users: %w", err) + } + + // Guest users + if err := s.db.Model(&models.User{}).Where("role = ?", models.RoleGuest).Count(&stats.Guest).Error; err != nil { + return nil, fmt.Errorf("failed to count guest users: %w", err) + } + + // Users with email + if err := s.db.Model(&models.User{}).Where("email != ''").Count(&stats.WithEmail).Error; err != nil { + return nil, fmt.Errorf("failed to count users with email: %w", err) + } + + return &stats, nil +} + +// AuthenticateUser authenticates a user with username and password +func (s *UserService) AuthenticateUser(username, password string) (*models.User, error) { + user, err := s.GetUserByUsername(username) + if err != nil { + return nil, errors.New("invalid username or password") + } + + if !user.IsActive() { + return nil, errors.New("user account is not active") + } + + if user.IsLocked() { + return nil, errors.New("user account is locked") + } + + if !user.VerifyPassword(password) { + user.FailedLoginAttempt() + if err := s.db.Save(user).Error; err != nil { + return nil, fmt.Errorf("failed to update failed login attempt: %w", err) + } + return nil, errors.New("invalid username or password") + } + + // Successful login + if err := user.Login(); err != nil { + return nil, fmt.Errorf("login failed: %w", err) + } + + if err := s.db.Save(user).Error; err != nil { + return nil, fmt.Errorf("failed to update login info: %w", err) + } + + return user, nil +} + +// ChangePassword changes a user's password +func (s *UserService) ChangePassword(id uuid.UUID, currentPassword, newPassword string) error { + user, err := s.GetUserByID(id) + if err != nil { + return err + } + + if !user.VerifyPassword(currentPassword) { + return errors.New("current password is incorrect") + } + + if err := user.SetPassword(newPassword); err != nil { + return fmt.Errorf("failed to set new password: %w", err) + } + + if err := s.db.Save(user).Error; err != nil { + return fmt.Errorf("failed to update password: %w", err) + } + + return nil +} + +// ResetPassword resets a user's password (admin function) +func (s *UserService) ResetPassword(id uuid.UUID, newPassword string) error { + user, err := s.GetUserByID(id) + if err != nil { + return err + } + + if err := user.SetPassword(newPassword); err != nil { + return fmt.Errorf("failed to set new password: %w", err) + } + + user.ResetLoginAttempts() + + if err := s.db.Save(user).Error; err != nil { + return fmt.Errorf("failed to update password: %w", err) + } + + return nil +} + +// AddPermission adds a permission to a user +func (s *UserService) AddPermission(id uuid.UUID, permission string) error { + user, err := s.GetUserByID(id) + if err != nil { + return err + } + + user.AddPermission(permission) + + if err := s.db.Save(user).Error; err != nil { + return fmt.Errorf("failed to add permission: %w", err) + } + + return nil +} + +// RemovePermission removes a permission from a user +func (s *UserService) RemovePermission(id uuid.UUID, permission string) error { + user, err := s.GetUserByID(id) + if err != nil { + return err + } + + user.RemovePermission(permission) + + if err := s.db.Save(user).Error; err != nil { + return fmt.Errorf("failed to remove permission: %w", err) + } + + return nil +} + +// ExportUsers exports users to JSON +func (s *UserService) ExportUsers() ([]byte, error) { + users, _, err := s.GetAllUsers(1, 1000) // Get all users (limit to 1000 for safety) + if err != nil { + return nil, fmt.Errorf("failed to get users for export: %w", err) + } + + var responses []*models.UserResponse + for _, user := range users { + responses = append(responses, user.ToResponse()) + } + + data, err := json.MarshalIndent(responses, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal users: %w", err) + } + + return data, nil +} + +// GetUserActivity returns user activity information +func (s *UserService) GetUserActivity(id uuid.UUID) (*utils.UserActivity, error) { + user, err := s.GetUserByID(id) + if err != nil { + return nil, err + } + + activity := &utils.UserActivity{ + UserID: user.ID, + Username: user.Username, + LastLogin: user.LastLogin, + LoginAttempts: user.LoginAttempts, + IsActive: user.IsActive(), + IsLocked: user.IsLocked(), + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + } + + return activity, nil +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/utils/types.go b/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/utils/types.go new file mode 100644 index 00000000..7d644744 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/go/user-management/internal/utils/types.go @@ -0,0 +1,250 @@ +package utils + +import ( + "time" + + "github.com/google/uuid" +) + +// UserStats represents user statistics +type UserStats struct { + Total int64 `json:"total"` + Active int64 `json:"active"` + Admin int64 `json:"admin"` + User int64 `json:"user"` + Guest int64 `json:"guest"` + WithEmail int64 `json:"with_email"` +} + +// UserActivity represents user activity information +type UserActivity struct { + UserID uuid.UUID `json:"user_id"` + Username string `json:"username"` + LastLogin *time.Time `json:"last_login"` + LoginAttempts int `json:"login_attempts"` + IsActive bool `json:"is_active"` + IsLocked bool `json:"is_locked"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// PaginatedResponse represents a paginated response +type PaginatedResponse struct { + Data interface{} `json:"data"` + Page int `json:"page"` + PageSize int `json:"page_size"` + Total int64 `json:"total"` + TotalPages int `json:"total_pages"` +} + +// NewPaginatedResponse creates a new paginated response +func NewPaginatedResponse(data interface{}, page, pageSize int, total int64) *PaginatedResponse { + totalPages := int((total + int64(pageSize) - 1) / int64(pageSize)) + return &PaginatedResponse{ + Data: data, + Page: page, + PageSize: pageSize, + Total: total, + TotalPages: totalPages, + } +} + +// APIResponse represents a standard API response +type APIResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +// NewSuccessResponse creates a new success response +func NewSuccessResponse(message string, data interface{}) *APIResponse { + return &APIResponse{ + Success: true, + Message: message, + Data: data, + } +} + +// NewErrorResponse creates a new error response +func NewErrorResponse(message string, err error) *APIResponse { + resp := &APIResponse{ + Success: false, + Message: message, + } + + if err != nil { + resp.Error = err.Error() + } + + return resp +} + +// ValidationError represents a validation error +type ValidationError struct { + Field string `json:"field"` + Message string `json:"message"` +} + +// ValidationErrors represents multiple validation errors +type ValidationErrors struct { + Errors []ValidationError `json:"errors"` +} + +// NewValidationErrors creates a new validation errors instance +func NewValidationErrors() *ValidationErrors { + return &ValidationErrors{ + Errors: make([]ValidationError, 0), + } +} + +// Add adds a validation error +func (ve *ValidationErrors) Add(field, message string) { + ve.Errors = append(ve.Errors, ValidationError{ + Field: field, + Message: message, + }) +} + +// HasErrors returns true if there are validation errors +func (ve *ValidationErrors) HasErrors() bool { + return len(ve.Errors) > 0 +} + +// Error implements the error interface +func (ve *ValidationErrors) Error() string { + if len(ve.Errors) == 0 { + return "" + } + + if len(ve.Errors) == 1 { + return ve.Errors[0].Message + } + + return "multiple validation errors" +} + +// DatabaseConfig represents database configuration +type DatabaseConfig struct { + Driver string `json:"driver"` + Host string `json:"host"` + Port int `json:"port"` + Database string `json:"database"` + Username string `json:"username"` + Password string `json:"password"` + SSLMode string `json:"ssl_mode"` +} + +// ServerConfig represents server configuration +type ServerConfig struct { + Port int `json:"port"` + Host string `json:"host"` + ReadTimeout int `json:"read_timeout"` + WriteTimeout int `json:"write_timeout"` + IdleTimeout int `json:"idle_timeout"` +} + +// JWTConfig represents JWT configuration +type JWTConfig struct { + SecretKey string `json:"secret_key"` + ExpirationHours int `json:"expiration_hours"` + RefreshHours int `json:"refresh_hours"` + Issuer string `json:"issuer"` + SigningAlgorithm string `json:"signing_algorithm"` +} + +// Config represents application configuration +type Config struct { + Database DatabaseConfig `json:"database"` + Server ServerConfig `json:"server"` + JWT JWTConfig `json:"jwt"` + LogLevel string `json:"log_level"` + Debug bool `json:"debug"` +} + +// SearchParams represents search parameters +type SearchParams struct { + Query string `json:"query"` + Page int `json:"page"` + PageSize int `json:"page_size"` + SortBy string `json:"sort_by"` + SortDir string `json:"sort_dir"` +} + +// NewSearchParams creates new search parameters with defaults +func NewSearchParams() *SearchParams { + return &SearchParams{ + Page: 1, + PageSize: 20, + SortBy: "created_at", + SortDir: "desc", + } +} + +// Validate validates search parameters +func (sp *SearchParams) Validate() error { + if sp.Page < 1 { + sp.Page = 1 + } + + if sp.PageSize < 1 { + sp.PageSize = 20 + } + + if sp.PageSize > 100 { + sp.PageSize = 100 + } + + if sp.SortBy == "" { + sp.SortBy = "created_at" + } + + if sp.SortDir != "asc" && sp.SortDir != "desc" { + sp.SortDir = "desc" + } + + return nil +} + +// FilterParams represents filter parameters +type FilterParams struct { + Role string `json:"role"` + Status string `json:"status"` + AgeMin int `json:"age_min"` + AgeMax int `json:"age_max"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AuditLog represents an audit log entry +type AuditLog struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + Action string `json:"action"` + Resource string `json:"resource"` + Details map[string]interface{} `json:"details"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + CreatedAt time.Time `json:"created_at"` +} + +// Session represents a user session +type Session struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// IsExpired checks if the session is expired +func (s *Session) IsExpired() bool { + return time.Now().After(s.ExpiresAt) +} + +// ExtendSession extends the session expiration +func (s *Session) ExtendSession(duration time.Duration) { + s.ExpiresAt = time.Now().Add(duration) + s.UpdatedAt = time.Now() +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/go/user-management/pkg/api/user_handler.go b/reference/code-index-mcp-master/test/sample-projects/go/user-management/pkg/api/user_handler.go new file mode 100644 index 00000000..1132c5a4 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/go/user-management/pkg/api/user_handler.go @@ -0,0 +1,309 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/example/user-management/internal/models" + "github.com/example/user-management/internal/services" + "github.com/example/user-management/internal/utils" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// UserHandler handles user-related HTTP requests +type UserHandler struct { + userService *services.UserService +} + +// NewUserHandler creates a new user handler +func NewUserHandler(userService *services.UserService) *UserHandler { + return &UserHandler{ + userService: userService, + } +} + +// CreateUser handles user creation +func (h *UserHandler) CreateUser(c *gin.Context) { + var req models.UserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err)) + return + } + + user, err := h.userService.CreateUser(&req) + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to create user", err)) + return + } + + c.JSON(http.StatusCreated, utils.NewSuccessResponse("User created successfully", user.ToResponse())) +} + +// GetUser handles getting a single user +func (h *UserHandler) GetUser(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err)) + return + } + + user, err := h.userService.GetUserByID(id) + if err != nil { + c.JSON(http.StatusNotFound, utils.NewErrorResponse("User not found", err)) + return + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("User retrieved successfully", user.ToResponse())) +} + +// GetUsers handles getting users with pagination +func (h *UserHandler) GetUsers(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + users, total, err := h.userService.GetAllUsers(page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to get users", err)) + return + } + + var responses []*models.UserResponse + for _, user := range users { + responses = append(responses, user.ToResponse()) + } + + paginatedResponse := utils.NewPaginatedResponse(responses, page, pageSize, total) + c.JSON(http.StatusOK, utils.NewSuccessResponse("Users retrieved successfully", paginatedResponse)) +} + +// UpdateUser handles user updates +func (h *UserHandler) UpdateUser(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err)) + return + } + + var updates map[string]interface{} + if err := c.ShouldBindJSON(&updates); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err)) + return + } + + user, err := h.userService.UpdateUser(id, updates) + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to update user", err)) + return + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("User updated successfully", user.ToResponse())) +} + +// DeleteUser handles user deletion +func (h *UserHandler) DeleteUser(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err)) + return + } + + if err := h.userService.DeleteUser(id); err != nil { + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to delete user", err)) + return + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("User deleted successfully", nil)) +} + +// SearchUsers handles user search +func (h *UserHandler) SearchUsers(c *gin.Context) { + query := c.Query("q") + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + users, total, err := h.userService.SearchUsers(query, page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to search users", err)) + return + } + + var responses []*models.UserResponse + for _, user := range users { + responses = append(responses, user.ToResponse()) + } + + paginatedResponse := utils.NewPaginatedResponse(responses, page, pageSize, total) + c.JSON(http.StatusOK, utils.NewSuccessResponse("Search completed successfully", paginatedResponse)) +} + +// GetUserStats handles getting user statistics +func (h *UserHandler) GetUserStats(c *gin.Context) { + stats, err := h.userService.GetUserStats() + if err != nil { + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to get user statistics", err)) + return + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("Statistics retrieved successfully", stats)) +} + +// ExportUsers handles user export +func (h *UserHandler) ExportUsers(c *gin.Context) { + data, err := h.userService.ExportUsers() + if err != nil { + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to export users", err)) + return + } + + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", "attachment; filename=users.json") + c.Data(http.StatusOK, "application/json", data) +} + +// Login handles user authentication +func (h *UserHandler) Login(c *gin.Context) { + var req struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err)) + return + } + + user, err := h.userService.AuthenticateUser(req.Username, req.Password) + if err != nil { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse("Authentication failed", err)) + return + } + + // In a real application, you would generate a JWT token here + response := map[string]interface{}{ + "user": user.ToResponse(), + "token": "dummy-jwt-token", // This would be a real JWT token + "expires": "2024-12-31T23:59:59Z", + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("Login successful", response)) +} + +// Logout handles user logout +func (h *UserHandler) Logout(c *gin.Context) { + // In a real application, you would invalidate the JWT token here + c.JSON(http.StatusOK, utils.NewSuccessResponse("Logout successful", nil)) +} + +// ChangePassword handles password change +func (h *UserHandler) ChangePassword(c *gin.Context) { + var req struct { + UserID uuid.UUID `json:"user_id" binding:"required"` + CurrentPassword string `json:"current_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=8"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err)) + return + } + + if err := h.userService.ChangePassword(req.UserID, req.CurrentPassword, req.NewPassword); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to change password", err)) + return + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("Password changed successfully", nil)) +} + +// ResetPassword handles password reset (admin only) +func (h *UserHandler) ResetPassword(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err)) + return + } + + var req struct { + NewPassword string `json:"new_password" binding:"required,min=8"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err)) + return + } + + if err := h.userService.ResetPassword(id, req.NewPassword); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to reset password", err)) + return + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("Password reset successfully", nil)) +} + +// AddPermission handles adding permission to user +func (h *UserHandler) AddPermission(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err)) + return + } + + var req struct { + Permission string `json:"permission" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err)) + return + } + + if err := h.userService.AddPermission(id, req.Permission); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to add permission", err)) + return + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("Permission added successfully", nil)) +} + +// RemovePermission handles removing permission from user +func (h *UserHandler) RemovePermission(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err)) + return + } + + permission := c.Query("permission") + if permission == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Permission parameter is required", nil)) + return + } + + if err := h.userService.RemovePermission(id, permission); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to remove permission", err)) + return + } + + c.JSON(http.StatusOK, utils.NewSuccessResponse("Permission removed successfully", nil)) +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/README.md b/reference/code-index-mcp-master/test/sample-projects/java/user-management/README.md new file mode 100644 index 00000000..f83a0a15 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/README.md @@ -0,0 +1,183 @@ +# User Management System (Java) + +A comprehensive user management system built in Java for testing Code Index MCP's analysis capabilities. + +## Features + +- **User Management**: Create, update, delete, and search users +- **Authentication**: BCrypt password hashing and verification +- **Authorization**: Role-based access control (Admin, User, Guest) +- **Data Validation**: Input validation and sanitization +- **Export/Import**: JSON and CSV export capabilities +- **Persistence**: File-based storage with JSON serialization +- **Logging**: SLF4J logging with Logback + +## Project Structure + +``` +src/main/java/com/example/usermanagement/ +├── models/ +│ ├── Person.java # Base person model +│ ├── User.java # User model with auth features +│ ├── UserRole.java # User role enumeration +│ └── UserStatus.java # User status enumeration +├── services/ +│ └── UserManager.java # User management service +├── utils/ +│ ├── ValidationUtils.java # Validation utilities +│ ├── UserNotFoundException.java # Custom exception +│ └── DuplicateUserException.java # Custom exception +└── Main.java # Main demo application +``` + +## Technologies Used + +- **Java 11**: Modern Java features and APIs +- **Jackson**: JSON processing and serialization +- **BCrypt**: Secure password hashing +- **Apache Commons**: Utility libraries (Lang3, CSV) +- **SLF4J + Logback**: Logging framework +- **Maven**: Build and dependency management +- **JUnit 5**: Testing framework + +## Build and Run + +### Prerequisites + +- Java 11 or higher +- Maven 3.6+ + +### Build + +```bash +mvn clean compile +``` + +### Run + +```bash +mvn exec:java -Dexec.mainClass="com.example.usermanagement.Main" +``` + +### Test + +```bash +mvn test +``` + +### Package + +```bash +mvn package +``` + +## Usage + +### Creating Users + +```java +UserManager userManager = new UserManager(); + +// Create a basic user +User user = userManager.createUser("John Doe", 30, "john_doe", "john@example.com"); +user.setPassword("SecurePass123!"); + +// Create an admin user +User admin = userManager.createUser("Jane Smith", 35, "jane_admin", + "jane@example.com", UserRole.ADMIN); +admin.setPassword("AdminPass123!"); +admin.addPermission("user_management"); +``` + +### User Authentication + +```java +// Verify password +boolean isValid = user.verifyPassword("SecurePass123!"); + +// Login +if (user.login()) { + System.out.println("Login successful!"); + System.out.println("Last login: " + user.getLastLogin()); +} +``` + +### User Management + +```java +// Search users +List results = userManager.searchUsers("john"); + +// Filter users +List activeUsers = userManager.getActiveUsers(); +List adminUsers = userManager.getUsersByRole(UserRole.ADMIN); +List olderUsers = userManager.getUsersOlderThan(25); + +// Update user +Map updates = Map.of("age", 31, "email", "newemail@example.com"); +userManager.updateUser("john_doe", updates); + +// Export users +String jsonData = userManager.exportUsers("json"); +String csvData = userManager.exportUsers("csv"); +``` + +## Testing Features + +This project tests the following Java language features: + +### Core Language Features +- **Classes and Inheritance**: Person and User class hierarchy +- **Enums**: UserRole and UserStatus with methods +- **Interfaces**: Custom exceptions and validation +- **Generics**: Collections with type safety +- **Annotations**: Jackson JSON annotations +- **Exception Handling**: Custom exceptions and try-catch blocks + +### Modern Java Features +- **Streams API**: Filtering, mapping, and collecting +- **Lambda Expressions**: Functional programming +- **Method References**: Stream operations +- **Optional**: Null-safe operations +- **Time API**: LocalDateTime usage + +### Advanced Features +- **Concurrent Collections**: ConcurrentHashMap +- **Reflection**: Jackson serialization +- **File I/O**: NIO.2 Path and Files +- **Logging**: SLF4J with parameterized messages +- **Validation**: Input validation and sanitization + +### Framework Integration +- **Maven**: Build lifecycle and dependency management +- **Jackson**: JSON serialization/deserialization +- **BCrypt**: Password hashing +- **Apache Commons**: Utility libraries +- **SLF4J**: Structured logging + +### Design Patterns +- **Builder Pattern**: Object construction +- **Factory Pattern**: User creation +- **Repository Pattern**: Data access +- **Service Layer**: Business logic separation + +## Dependencies + +### Core Dependencies +- **Jackson Databind**: JSON processing +- **Jackson JSR310**: Java 8 time support +- **BCrypt**: Password hashing +- **Apache Commons Lang3**: Utilities +- **Apache Commons CSV**: CSV processing + +### Logging +- **SLF4J API**: Logging facade +- **Logback Classic**: Logging implementation + +### Testing +- **JUnit 5**: Testing framework +- **Mockito**: Mocking framework + +## License + +MIT License - This is a sample project for testing purposes. \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/pom.xml b/reference/code-index-mcp-master/test/sample-projects/java/user-management/pom.xml new file mode 100644 index 00000000..1287b943 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + com.example + user-management + 1.0.0 + jar + + User Management System + A sample user management system for testing Code Index MCP + + + 11 + 11 + UTF-8 + 5.9.2 + 2.15.2 + 2.0.7 + 1.4.7 + + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + org.apache.commons + commons-csv + 1.9.0 + + + + + org.mindrot + jbcrypt + 0.4 + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + org.mockito + mockito-core + 5.3.1 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 11 + 11 + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.example.usermanagement.Main + + + + + \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/Main.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/Main.java new file mode 100644 index 00000000..b74e7dbe --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/Main.java @@ -0,0 +1,220 @@ +package com.example.usermanagement; + +import com.example.usermanagement.models.User; +import com.example.usermanagement.models.UserRole; +import com.example.usermanagement.services.UserManager; +import com.example.usermanagement.utils.UserNotFoundException; +import com.example.usermanagement.utils.DuplicateUserException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Main class demonstrating the User Management System. + */ +public class Main { + + private static final Logger logger = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) { + System.out.println("=".repeat(50)); + System.out.println("User Management System Demo (Java)"); + System.out.println("=".repeat(50)); + + // Create user manager + UserManager userManager = new UserManager(); + + // Create sample users + System.out.println("\n1. Creating sample users..."); + createSampleUsers(userManager); + + // Display all users + System.out.println("\n2. Listing all users..."); + listAllUsers(userManager); + + // Test user retrieval + System.out.println("\n3. Testing user retrieval..."); + testUserRetrieval(userManager); + + // Test user search + System.out.println("\n4. Testing user search..."); + testUserSearch(userManager); + + // Test user filtering + System.out.println("\n5. Testing user filtering..."); + testUserFiltering(userManager); + + // Test user updates + System.out.println("\n6. Testing user updates..."); + testUserUpdates(userManager); + + // Test authentication + System.out.println("\n7. Testing authentication..."); + testAuthentication(userManager); + + // Display statistics + System.out.println("\n8. User statistics..."); + displayStatistics(userManager); + + // Test export functionality + System.out.println("\n9. Testing export functionality..."); + testExport(userManager); + + // Test user permissions + System.out.println("\n10. Testing user permissions..."); + testPermissions(userManager); + + System.out.println("\n" + "=".repeat(50)); + System.out.println("Demo completed successfully!"); + System.out.println("=".repeat(50)); + } + + private static void createSampleUsers(UserManager userManager) { + try { + // Create admin user + User admin = userManager.createUser("Alice Johnson", 30, "alice_admin", + "alice@example.com", UserRole.ADMIN); + admin.setPassword("AdminPass123!"); + admin.addPermission("user_management"); + admin.addPermission("system_admin"); + + // Create regular users + User user1 = userManager.createUser("Bob Smith", 25, "bob_user", "bob@example.com"); + user1.setPassword("UserPass123!"); + + User user2 = userManager.createUser("Charlie Brown", 35, "charlie", "charlie@example.com"); + user2.setPassword("CharliePass123!"); + + User user3 = userManager.createUser("Diana Prince", 28, "diana", "diana@example.com"); + user3.setPassword("DianaPass123!"); + + System.out.println("✓ Created " + userManager.getUserCount() + " users"); + + } catch (DuplicateUserException e) { + System.out.println("✗ Error creating users: " + e.getMessage()); + } catch (Exception e) { + System.out.println("✗ Unexpected error: " + e.getMessage()); + logger.error("Error creating sample users", e); + } + } + + private static void listAllUsers(UserManager userManager) { + List users = userManager.getAllUsers(); + + System.out.println("Found " + users.size() + " users:"); + users.forEach(user -> + System.out.println(" • " + user.getUsername() + " (" + user.getName() + + ") - " + user.getRole().getDisplayName() + + " [" + user.getStatus().getDisplayName() + "]") + ); + } + + private static void testUserRetrieval(UserManager userManager) { + try { + User user = userManager.getUser("alice_admin"); + System.out.println("✓ Retrieved user: " + user.getUsername() + " (" + user.getName() + ")"); + + User userByEmail = userManager.getUserByEmail("bob@example.com"); + if (userByEmail != null) { + System.out.println("✓ Found user by email: " + userByEmail.getUsername()); + } + + } catch (UserNotFoundException e) { + System.out.println("✗ User retrieval failed: " + e.getMessage()); + } + } + + private static void testUserSearch(UserManager userManager) { + List searchResults = userManager.searchUsers("alice"); + System.out.println("Search results for 'alice': " + searchResults.size() + " users found"); + + searchResults.forEach(user -> + System.out.println(" • " + user.getUsername() + " (" + user.getName() + ")") + ); + } + + private static void testUserFiltering(UserManager userManager) { + List olderUsers = userManager.getUsersOlderThan(30); + System.out.println("Users older than 30: " + olderUsers.size() + " users"); + + olderUsers.forEach(user -> + System.out.println(" • " + user.getUsername() + " (" + user.getName() + ") - age " + user.getAge()) + ); + + List adminUsers = userManager.getUsersByRole(UserRole.ADMIN); + System.out.println("Admin users: " + adminUsers.size() + " users"); + } + + private static void testUserUpdates(UserManager userManager) { + try { + Map updates = Map.of("age", 26); + User updatedUser = userManager.updateUser("bob_user", updates); + System.out.println("✓ Updated " + updatedUser.getUsername() + "'s age to " + updatedUser.getAge()); + + } catch (UserNotFoundException e) { + System.out.println("✗ Update failed: " + e.getMessage()); + } + } + + private static void testAuthentication(UserManager userManager) { + try { + User user = userManager.getUser("alice_admin"); + + // Test password verification + boolean isValid = user.verifyPassword("AdminPass123!"); + System.out.println("✓ Password verification: " + (isValid ? "SUCCESS" : "FAILED")); + + // Test login + boolean loginSuccess = user.login(); + System.out.println("✓ Login attempt: " + (loginSuccess ? "SUCCESS" : "FAILED")); + + if (loginSuccess) { + System.out.println("✓ Last login: " + user.getLastLogin()); + } + + } catch (UserNotFoundException e) { + System.out.println("✗ Authentication test failed: " + e.getMessage()); + } + } + + private static void displayStatistics(UserManager userManager) { + Map stats = userManager.getUserStats(); + + stats.forEach((key, value) -> + System.out.println(" " + key.replace("_", " ").toUpperCase() + ": " + value) + ); + } + + private static void testExport(UserManager userManager) { + try { + String jsonExport = userManager.exportUsers("json"); + System.out.println("✓ JSON export: " + jsonExport.length() + " characters"); + + String csvExport = userManager.exportUsers("csv"); + System.out.println("✓ CSV export: " + csvExport.split("\n").length + " lines"); + + } catch (Exception e) { + System.out.println("✗ Export failed: " + e.getMessage()); + } + } + + private static void testPermissions(UserManager userManager) { + try { + User admin = userManager.getUser("alice_admin"); + + System.out.println("Admin permissions: " + admin.getPermissions()); + System.out.println("Has user_management permission: " + admin.hasPermission("user_management")); + System.out.println("Is admin: " + admin.isAdmin()); + + // Test role privileges + System.out.println("Admin role can act on USER role: " + + admin.getRole().canActOn(UserRole.USER)); + + } catch (UserNotFoundException e) { + System.out.println("✗ Permission test failed: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/Person.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/Person.java new file mode 100644 index 00000000..01cb07eb --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/Person.java @@ -0,0 +1,284 @@ +package com.example.usermanagement.models; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.StringUtils; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a person with basic information. + * This class serves as the base class for more specific person types. + */ +public class Person { + + @JsonProperty("name") + private String name; + + @JsonProperty("age") + private int age; + + @JsonProperty("email") + private String email; + + @JsonProperty("created_at") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime createdAt; + + @JsonProperty("metadata") + private Map metadata; + + /** + * Default constructor for Jackson deserialization. + */ + public Person() { + this.createdAt = LocalDateTime.now(); + this.metadata = new HashMap<>(); + } + + /** + * Constructor with name and age. + * + * @param name The person's name + * @param age The person's age + * @throws IllegalArgumentException if validation fails + */ + public Person(String name, int age) { + this(); + setName(name); + setAge(age); + } + + /** + * Constructor with name, age, and email. + * + * @param name The person's name + * @param age The person's age + * @param email The person's email address + * @throws IllegalArgumentException if validation fails + */ + public Person(String name, int age, String email) { + this(name, age); + setEmail(email); + } + + // Getters and Setters + + public String getName() { + return name; + } + + public void setName(String name) { + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Name cannot be null or empty"); + } + if (name.length() > 100) { + throw new IllegalArgumentException("Name cannot exceed 100 characters"); + } + this.name = name.trim(); + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + if (age < 0) { + throw new IllegalArgumentException("Age cannot be negative"); + } + if (age > 150) { + throw new IllegalArgumentException("Age cannot exceed 150"); + } + this.age = age; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + if (StringUtils.isNotBlank(email) && !isValidEmail(email)) { + throw new IllegalArgumentException("Invalid email format"); + } + this.email = StringUtils.isBlank(email) ? null : email.trim(); + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public Map getMetadata() { + return new HashMap<>(metadata); + } + + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new HashMap<>() : new HashMap<>(metadata); + } + + // Business methods + + /** + * Returns a greeting message for the person. + * + * @return A personalized greeting + */ + public String greet() { + return String.format("Hello, I'm %s and I'm %d years old.", name, age); + } + + /** + * Checks if the person has an email address. + * + * @return true if email is present and not empty + */ + public boolean hasEmail() { + return StringUtils.isNotBlank(email); + } + + /** + * Updates the person's email address. + * + * @param newEmail The new email address + * @throws IllegalArgumentException if email format is invalid + */ + public void updateEmail(String newEmail) { + setEmail(newEmail); + } + + /** + * Adds metadata to the person. + * + * @param key The metadata key + * @param value The metadata value + */ + public void addMetadata(String key, Object value) { + if (StringUtils.isNotBlank(key)) { + metadata.put(key, value); + } + } + + /** + * Gets metadata value by key. + * + * @param key The metadata key + * @return The metadata value or null if not found + */ + public Object getMetadata(String key) { + return metadata.get(key); + } + + /** + * Gets metadata value by key with default value. + * + * @param key The metadata key + * @param defaultValue The default value if key is not found + * @return The metadata value or default value + */ + public Object getMetadata(String key, Object defaultValue) { + return metadata.getOrDefault(key, defaultValue); + } + + /** + * Removes metadata by key. + * + * @param key The metadata key to remove + * @return The removed value or null if not found + */ + public Object removeMetadata(String key) { + return metadata.remove(key); + } + + /** + * Clears all metadata. + */ + public void clearMetadata() { + metadata.clear(); + } + + /** + * Validates email format using a simple regex. + * + * @param email The email to validate + * @return true if email format is valid + */ + private boolean isValidEmail(String email) { + String emailPattern = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; + return email.matches(emailPattern); + } + + /** + * Creates a Person instance from a map of data. + * + * @param data The data map + * @return A new Person instance + */ + public static Person fromMap(Map data) { + Person person = new Person(); + + if (data.containsKey("name")) { + person.setName((String) data.get("name")); + } + + if (data.containsKey("age")) { + person.setAge((Integer) data.get("age")); + } + + if (data.containsKey("email")) { + person.setEmail((String) data.get("email")); + } + + if (data.containsKey("metadata")) { + @SuppressWarnings("unchecked") + Map metadata = (Map) data.get("metadata"); + person.setMetadata(metadata); + } + + return person; + } + + /** + * Converts the person to a map representation. + * + * @return A map containing person data + */ + public Map toMap() { + Map map = new HashMap<>(); + map.put("name", name); + map.put("age", age); + map.put("email", email); + map.put("created_at", createdAt); + map.put("metadata", new HashMap<>(metadata)); + return map; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + Person person = (Person) obj; + return age == person.age && + Objects.equals(name, person.name) && + Objects.equals(email, person.email) && + Objects.equals(createdAt, person.createdAt) && + Objects.equals(metadata, person.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(name, age, email, createdAt, metadata); + } + + @Override + public String toString() { + return String.format("Person{name='%s', age=%d, email='%s', createdAt=%s}", + name, age, email, createdAt); + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/User.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/User.java new file mode 100644 index 00000000..b68c72b9 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/User.java @@ -0,0 +1,363 @@ +package com.example.usermanagement.models; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.StringUtils; +import org.mindrot.jbcrypt.BCrypt; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; +import java.util.Objects; + +/** + * User class extending Person with authentication and authorization features. + */ +public class User extends Person { + + @JsonProperty("username") + private String username; + + @JsonProperty("password_hash") + private String passwordHash; + + @JsonProperty("role") + private UserRole role; + + @JsonProperty("status") + private UserStatus status; + + @JsonProperty("last_login") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime lastLogin; + + @JsonProperty("login_attempts") + private int loginAttempts; + + @JsonProperty("permissions") + private Set permissions; + + /** + * Default constructor for Jackson deserialization. + */ + public User() { + super(); + this.role = UserRole.USER; + this.status = UserStatus.ACTIVE; + this.loginAttempts = 0; + this.permissions = new HashSet<>(); + } + + /** + * Constructor with basic information. + * + * @param name The user's name + * @param age The user's age + * @param username The username + */ + public User(String name, int age, String username) { + super(name, age); + setUsername(username); + this.role = UserRole.USER; + this.status = UserStatus.ACTIVE; + this.loginAttempts = 0; + this.permissions = new HashSet<>(); + } + + /** + * Constructor with email. + * + * @param name The user's name + * @param age The user's age + * @param username The username + * @param email The email address + */ + public User(String name, int age, String username, String email) { + super(name, age, email); + setUsername(username); + this.role = UserRole.USER; + this.status = UserStatus.ACTIVE; + this.loginAttempts = 0; + this.permissions = new HashSet<>(); + } + + /** + * Constructor with role. + * + * @param name The user's name + * @param age The user's age + * @param username The username + * @param email The email address + * @param role The user role + */ + public User(String name, int age, String username, String email, UserRole role) { + this(name, age, username, email); + this.role = role; + } + + // Getters and Setters + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + if (StringUtils.isBlank(username)) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + if (username.length() < 3 || username.length() > 20) { + throw new IllegalArgumentException("Username must be between 3 and 20 characters"); + } + if (!username.matches("^[a-zA-Z0-9_]+$")) { + throw new IllegalArgumentException("Username can only contain letters, numbers, and underscores"); + } + this.username = username.trim(); + } + + public String getPasswordHash() { + return passwordHash; + } + + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + public UserRole getRole() { + return role; + } + + public void setRole(UserRole role) { + this.role = role != null ? role : UserRole.USER; + } + + public UserStatus getStatus() { + return status; + } + + public void setStatus(UserStatus status) { + this.status = status != null ? status : UserStatus.ACTIVE; + } + + public LocalDateTime getLastLogin() { + return lastLogin; + } + + public void setLastLogin(LocalDateTime lastLogin) { + this.lastLogin = lastLogin; + } + + public int getLoginAttempts() { + return loginAttempts; + } + + public void setLoginAttempts(int loginAttempts) { + this.loginAttempts = Math.max(0, loginAttempts); + } + + public Set getPermissions() { + return new HashSet<>(permissions); + } + + public void setPermissions(Set permissions) { + this.permissions = permissions != null ? new HashSet<>(permissions) : new HashSet<>(); + } + + // Authentication methods + + /** + * Sets the user's password using BCrypt hashing. + * + * @param password The plain text password + * @throws IllegalArgumentException if password is invalid + */ + public void setPassword(String password) { + if (StringUtils.isBlank(password)) { + throw new IllegalArgumentException("Password cannot be null or empty"); + } + if (password.length() < 8) { + throw new IllegalArgumentException("Password must be at least 8 characters long"); + } + + // Hash the password with BCrypt + this.passwordHash = BCrypt.hashpw(password, BCrypt.gensalt()); + } + + /** + * Verifies a password against the stored hash. + * + * @param password The plain text password to verify + * @return true if password matches + */ + public boolean verifyPassword(String password) { + if (StringUtils.isBlank(password) || StringUtils.isBlank(passwordHash)) { + return false; + } + + try { + return BCrypt.checkpw(password, passwordHash); + } catch (IllegalArgumentException e) { + return false; + } + } + + // Permission methods + + /** + * Adds a permission to the user. + * + * @param permission The permission to add + */ + public void addPermission(String permission) { + if (StringUtils.isNotBlank(permission)) { + permissions.add(permission.trim()); + } + } + + /** + * Removes a permission from the user. + * + * @param permission The permission to remove + */ + public void removePermission(String permission) { + permissions.remove(permission); + } + + /** + * Checks if the user has a specific permission. + * + * @param permission The permission to check + * @return true if user has the permission + */ + public boolean hasPermission(String permission) { + return permissions.contains(permission); + } + + /** + * Clears all permissions. + */ + public void clearPermissions() { + permissions.clear(); + } + + // Status and role methods + + /** + * Checks if the user is an admin. + * + * @return true if user is admin + */ + public boolean isAdmin() { + return role == UserRole.ADMIN; + } + + /** + * Checks if the user is active. + * + * @return true if user is active + */ + public boolean isActive() { + return status == UserStatus.ACTIVE; + } + + /** + * Checks if the user is locked due to too many failed login attempts. + * + * @return true if user is locked + */ + public boolean isLocked() { + return status == UserStatus.SUSPENDED || loginAttempts >= 5; + } + + // Login methods + + /** + * Records a successful login. + * + * @return true if login was successful + */ + public boolean login() { + if (!isActive() || isLocked()) { + return false; + } + + this.lastLogin = LocalDateTime.now(); + this.loginAttempts = 0; + return true; + } + + /** + * Records a failed login attempt. + */ + public void failedLoginAttempt() { + this.loginAttempts++; + if (this.loginAttempts >= 5) { + this.status = UserStatus.SUSPENDED; + } + } + + /** + * Resets login attempts. + */ + public void resetLoginAttempts() { + this.loginAttempts = 0; + } + + // Status change methods + + /** + * Activates the user account. + */ + public void activate() { + this.status = UserStatus.ACTIVE; + this.loginAttempts = 0; + } + + /** + * Deactivates the user account. + */ + public void deactivate() { + this.status = UserStatus.INACTIVE; + } + + /** + * Suspends the user account. + */ + public void suspend() { + this.status = UserStatus.SUSPENDED; + } + + /** + * Marks the user as deleted. + */ + public void delete() { + this.status = UserStatus.DELETED; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + if (!super.equals(obj)) return false; + + User user = (User) obj; + return loginAttempts == user.loginAttempts && + Objects.equals(username, user.username) && + Objects.equals(passwordHash, user.passwordHash) && + role == user.role && + status == user.status && + Objects.equals(lastLogin, user.lastLogin) && + Objects.equals(permissions, user.permissions); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), username, passwordHash, role, status, + lastLogin, loginAttempts, permissions); + } + + @Override + public String toString() { + return String.format("User{username='%s', name='%s', role=%s, status=%s, lastLogin=%s}", + username, getName(), role, status, lastLogin); + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/UserRole.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/UserRole.java new file mode 100644 index 00000000..492f3bef --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/UserRole.java @@ -0,0 +1,134 @@ +package com.example.usermanagement.models; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Enumeration for user roles in the system. + */ +public enum UserRole { + + /** + * Administrator role with full system access. + */ + ADMIN("admin", "Administrator", "Full system access"), + + /** + * Regular user role with standard permissions. + */ + USER("user", "User", "Standard user permissions"), + + /** + * Guest role with limited permissions. + */ + GUEST("guest", "Guest", "Limited guest permissions"); + + private final String code; + private final String displayName; + private final String description; + + /** + * Constructor for UserRole enum. + * + * @param code The role code + * @param displayName The display name + * @param description The role description + */ + UserRole(String code, String displayName, String description) { + this.code = code; + this.displayName = displayName; + this.description = description; + } + + /** + * Gets the role code. + * + * @return The role code + */ + @JsonValue + public String getCode() { + return code; + } + + /** + * Gets the display name. + * + * @return The display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * Gets the role description. + * + * @return The role description + */ + public String getDescription() { + return description; + } + + /** + * Finds a UserRole by its code. + * + * @param code The role code to search for + * @return The UserRole or null if not found + */ + public static UserRole fromCode(String code) { + if (code == null) { + return null; + } + + for (UserRole role : values()) { + if (role.code.equalsIgnoreCase(code)) { + return role; + } + } + return null; + } + + /** + * Checks if this role has higher privilege than another role. + * + * @param other The other role to compare with + * @return true if this role has higher privilege + */ + public boolean hasHigherPrivilegeThan(UserRole other) { + return this.ordinal() < other.ordinal(); + } + + /** + * Checks if this role has lower privilege than another role. + * + * @param other The other role to compare with + * @return true if this role has lower privilege + */ + public boolean hasLowerPrivilegeThan(UserRole other) { + return this.ordinal() > other.ordinal(); + } + + /** + * Checks if this role can perform actions on another role. + * + * @param targetRole The target role + * @return true if this role can act on the target role + */ + public boolean canActOn(UserRole targetRole) { + // Admin can act on all roles + if (this == ADMIN) { + return true; + } + + // Users can only act on guests + if (this == USER) { + return targetRole == GUEST; + } + + // Guests cannot act on anyone + return false; + } + + @Override + public String toString() { + return displayName; + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/UserStatus.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/UserStatus.java new file mode 100644 index 00000000..1166e182 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/models/UserStatus.java @@ -0,0 +1,146 @@ +package com.example.usermanagement.models; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Enumeration for user status in the system. + */ +public enum UserStatus { + + /** + * Active status - user can login and use the system. + */ + ACTIVE("active", "Active", "User can login and use the system"), + + /** + * Inactive status - user account is temporarily disabled. + */ + INACTIVE("inactive", "Inactive", "User account is temporarily disabled"), + + /** + * Suspended status - user account is suspended due to violations. + */ + SUSPENDED("suspended", "Suspended", "User account is suspended due to violations"), + + /** + * Deleted status - user account is marked for deletion. + */ + DELETED("deleted", "Deleted", "User account is marked for deletion"); + + private final String code; + private final String displayName; + private final String description; + + /** + * Constructor for UserStatus enum. + * + * @param code The status code + * @param displayName The display name + * @param description The status description + */ + UserStatus(String code, String displayName, String description) { + this.code = code; + this.displayName = displayName; + this.description = description; + } + + /** + * Gets the status code. + * + * @return The status code + */ + @JsonValue + public String getCode() { + return code; + } + + /** + * Gets the display name. + * + * @return The display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * Gets the status description. + * + * @return The status description + */ + public String getDescription() { + return description; + } + + /** + * Finds a UserStatus by its code. + * + * @param code The status code to search for + * @return The UserStatus or null if not found + */ + public static UserStatus fromCode(String code) { + if (code == null) { + return null; + } + + for (UserStatus status : values()) { + if (status.code.equalsIgnoreCase(code)) { + return status; + } + } + return null; + } + + /** + * Checks if this status allows user login. + * + * @return true if user can login with this status + */ + public boolean allowsLogin() { + return this == ACTIVE; + } + + /** + * Checks if this status indicates the user is disabled. + * + * @return true if user is disabled + */ + public boolean isDisabled() { + return this == INACTIVE || this == SUSPENDED || this == DELETED; + } + + /** + * Checks if this status indicates the user is deleted. + * + * @return true if user is deleted + */ + public boolean isDeleted() { + return this == DELETED; + } + + /** + * Checks if this status can be changed to another status. + * + * @param targetStatus The target status + * @return true if status change is allowed + */ + public boolean canChangeTo(UserStatus targetStatus) { + // Cannot change from deleted status + if (this == DELETED) { + return false; + } + + // Cannot change to same status + if (this == targetStatus) { + return false; + } + + // All other changes are allowed + return true; + } + + @Override + public String toString() { + return displayName; + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/services/UserManager.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/services/UserManager.java new file mode 100644 index 00000000..ca32e116 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/services/UserManager.java @@ -0,0 +1,488 @@ +package com.example.usermanagement.services; + +import com.example.usermanagement.models.User; +import com.example.usermanagement.models.UserRole; +import com.example.usermanagement.models.UserStatus; +import com.example.usermanagement.utils.UserNotFoundException; +import com.example.usermanagement.utils.DuplicateUserException; +import com.example.usermanagement.utils.ValidationUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Service class for managing users in the system. + * Provides CRUD operations, search functionality, and data persistence. + */ +public class UserManager { + + private static final Logger logger = LoggerFactory.getLogger(UserManager.class); + + private final Map users; + private final ObjectMapper objectMapper; + private final String storagePath; + + /** + * Constructor with default storage path. + */ + public UserManager() { + this(null); + } + + /** + * Constructor with custom storage path. + * + * @param storagePath The file path for user data storage + */ + public UserManager(String storagePath) { + this.users = new ConcurrentHashMap<>(); + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new JavaTimeModule()); + this.storagePath = storagePath; + + if (StringUtils.isNotBlank(storagePath)) { + loadUsersFromFile(); + } + } + + /** + * Creates a new user in the system. + * + * @param name The user's name + * @param age The user's age + * @param username The username + * @param email The email address (optional) + * @param role The user role + * @return The created user + * @throws DuplicateUserException if username already exists + * @throws IllegalArgumentException if validation fails + */ + public User createUser(String name, int age, String username, String email, UserRole role) { + logger.debug("Creating user with username: {}", username); + + if (users.containsKey(username)) { + throw new DuplicateUserException("User with username '" + username + "' already exists"); + } + + // Validate inputs + ValidationUtils.validateUsername(username); + if (StringUtils.isNotBlank(email)) { + ValidationUtils.validateEmail(email); + } + + User user = new User(name, age, username, email, role); + users.put(username, user); + + saveUsersToFile(); + logger.info("User created successfully: {}", username); + + return user; + } + + /** + * Creates a new user with default role. + * + * @param name The user's name + * @param age The user's age + * @param username The username + * @param email The email address (optional) + * @return The created user + */ + public User createUser(String name, int age, String username, String email) { + return createUser(name, age, username, email, UserRole.USER); + } + + /** + * Creates a new user with minimal information. + * + * @param name The user's name + * @param age The user's age + * @param username The username + * @return The created user + */ + public User createUser(String name, int age, String username) { + return createUser(name, age, username, null, UserRole.USER); + } + + /** + * Retrieves a user by username. + * + * @param username The username + * @return The user + * @throws UserNotFoundException if user is not found + */ + public User getUser(String username) { + User user = users.get(username); + if (user == null) { + throw new UserNotFoundException("User with username '" + username + "' not found"); + } + return user; + } + + /** + * Retrieves a user by email address. + * + * @param email The email address + * @return The user or null if not found + */ + public User getUserByEmail(String email) { + return users.values().stream() + .filter(user -> Objects.equals(user.getEmail(), email)) + .findFirst() + .orElse(null); + } + + /** + * Updates user information. + * + * @param username The username + * @param updates A map of field updates + * @return The updated user + * @throws UserNotFoundException if user is not found + */ + public User updateUser(String username, Map updates) { + User user = getUser(username); + + updates.forEach((field, value) -> { + switch (field.toLowerCase()) { + case "name": + user.setName((String) value); + break; + case "age": + user.setAge((Integer) value); + break; + case "email": + user.setEmail((String) value); + break; + case "role": + if (value instanceof UserRole) { + user.setRole((UserRole) value); + } else if (value instanceof String) { + user.setRole(UserRole.fromCode((String) value)); + } + break; + case "status": + if (value instanceof UserStatus) { + user.setStatus((UserStatus) value); + } else if (value instanceof String) { + user.setStatus(UserStatus.fromCode((String) value)); + } + break; + default: + logger.warn("Unknown field for update: {}", field); + } + }); + + saveUsersToFile(); + logger.info("User updated successfully: {}", username); + + return user; + } + + /** + * Deletes a user (soft delete). + * + * @param username The username + * @return true if user was deleted + * @throws UserNotFoundException if user is not found + */ + public boolean deleteUser(String username) { + User user = getUser(username); + user.delete(); + + saveUsersToFile(); + logger.info("User deleted successfully: {}", username); + + return true; + } + + /** + * Removes a user completely from the system. + * + * @param username The username + * @return true if user was removed + * @throws UserNotFoundException if user is not found + */ + public boolean removeUser(String username) { + if (!users.containsKey(username)) { + throw new UserNotFoundException("User with username '" + username + "' not found"); + } + + users.remove(username); + saveUsersToFile(); + logger.info("User removed completely: {}", username); + + return true; + } + + /** + * Gets all users in the system. + * + * @return A list of all users + */ + public List getAllUsers() { + return new ArrayList<>(users.values()); + } + + /** + * Gets all active users. + * + * @return A list of active users + */ + public List getActiveUsers() { + return users.values().stream() + .filter(User::isActive) + .collect(Collectors.toList()); + } + + /** + * Gets users by role. + * + * @param role The user role + * @return A list of users with the specified role + */ + public List getUsersByRole(UserRole role) { + return users.values().stream() + .filter(user -> user.getRole() == role) + .collect(Collectors.toList()); + } + + /** + * Filters users using a custom predicate. + * + * @param predicate The filter predicate + * @return A list of filtered users + */ + public List filterUsers(Predicate predicate) { + return users.values().stream() + .filter(predicate) + .collect(Collectors.toList()); + } + + /** + * Searches users by name or username. + * + * @param query The search query + * @return A list of matching users + */ + public List searchUsers(String query) { + if (StringUtils.isBlank(query)) { + return new ArrayList<>(); + } + + String lowercaseQuery = query.toLowerCase(); + return users.values().stream() + .filter(user -> + user.getName().toLowerCase().contains(lowercaseQuery) || + user.getUsername().toLowerCase().contains(lowercaseQuery) || + (user.getEmail() != null && user.getEmail().toLowerCase().contains(lowercaseQuery))) + .collect(Collectors.toList()); + } + + /** + * Gets users older than specified age. + * + * @param age The age threshold + * @return A list of users older than the specified age + */ + public List getUsersOlderThan(int age) { + return filterUsers(user -> user.getAge() > age); + } + + /** + * Gets users with email addresses. + * + * @return A list of users with email addresses + */ + public List getUsersWithEmail() { + return filterUsers(User::hasEmail); + } + + /** + * Gets users with specific permission. + * + * @param permission The permission to check + * @return A list of users with the specified permission + */ + public List getUsersWithPermission(String permission) { + return filterUsers(user -> user.hasPermission(permission)); + } + + /** + * Gets the total number of users. + * + * @return The user count + */ + public int getUserCount() { + return users.size(); + } + + /** + * Gets user statistics. + * + * @return A map of user statistics + */ + public Map getUserStats() { + Map stats = new HashMap<>(); + + stats.put("total", users.size()); + stats.put("active", getActiveUsers().size()); + stats.put("admin", getUsersByRole(UserRole.ADMIN).size()); + stats.put("user", getUsersByRole(UserRole.USER).size()); + stats.put("guest", getUsersByRole(UserRole.GUEST).size()); + stats.put("with_email", getUsersWithEmail().size()); + + return stats; + } + + /** + * Exports users to specified format. + * + * @param format The export format ("json" or "csv") + * @return The exported data as string + * @throws IllegalArgumentException if format is unsupported + */ + public String exportUsers(String format) { + switch (format.toLowerCase()) { + case "json": + return exportToJson(); + case "csv": + return exportToCsv(); + default: + throw new IllegalArgumentException("Unsupported export format: " + format); + } + } + + /** + * Exports users to JSON format. + * + * @return JSON string representation of users + */ + private String exportToJson() { + try { + return objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(users.values()); + } catch (JsonProcessingException e) { + logger.error("Error exporting users to JSON", e); + return "[]"; + } + } + + /** + * Exports users to CSV format. + * + * @return CSV string representation of users + */ + private String exportToCsv() { + try (StringWriter writer = new StringWriter(); + CSVPrinter printer = new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader( + "Username", "Name", "Age", "Email", "Role", "Status", "Last Login"))) { + + for (User user : users.values()) { + printer.printRecord( + user.getUsername(), + user.getName(), + user.getAge(), + user.getEmail(), + user.getRole().getCode(), + user.getStatus().getCode(), + user.getLastLogin() + ); + } + + return writer.toString(); + } catch (IOException e) { + logger.error("Error exporting users to CSV", e); + return "Username,Name,Age,Email,Role,Status,Last Login\n"; + } + } + + /** + * Checks if a username exists in the system. + * + * @param username The username to check + * @return true if username exists + */ + public boolean userExists(String username) { + return users.containsKey(username); + } + + /** + * Clears all users from the system. + */ + public void clearAllUsers() { + users.clear(); + saveUsersToFile(); + logger.info("All users cleared from system"); + } + + /** + * Loads users from file storage. + */ + private void loadUsersFromFile() { + if (StringUtils.isBlank(storagePath)) { + return; + } + + try { + Path path = Paths.get(storagePath); + if (!Files.exists(path)) { + logger.debug("User storage file does not exist: {}", storagePath); + return; + } + + String content = Files.readString(path); + List userList = Arrays.asList(objectMapper.readValue(content, User[].class)); + + users.clear(); + for (User user : userList) { + users.put(user.getUsername(), user); + } + + logger.info("Loaded {} users from file: {}", users.size(), storagePath); + } catch (IOException e) { + logger.error("Error loading users from file: {}", storagePath, e); + } + } + + /** + * Saves users to file storage. + */ + private void saveUsersToFile() { + if (StringUtils.isBlank(storagePath)) { + return; + } + + try { + Path path = Paths.get(storagePath); + Files.createDirectories(path.getParent()); + + String content = objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(users.values()); + Files.writeString(path, content); + + logger.debug("Saved {} users to file: {}", users.size(), storagePath); + } catch (IOException e) { + logger.error("Error saving users to file: {}", storagePath, e); + } + } + + // CI marker method to verify auto-reindex on change + public String ciAddedSymbolMarker() { + return "ci_symbol_java"; + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/DuplicateUserException.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/DuplicateUserException.java new file mode 100644 index 00000000..e0601e54 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/DuplicateUserException.java @@ -0,0 +1,26 @@ +package com.example.usermanagement.utils; + +/** + * Exception thrown when attempting to create a user that already exists. + */ +public class DuplicateUserException extends RuntimeException { + + /** + * Constructs a new DuplicateUserException with the specified detail message. + * + * @param message the detail message + */ + public DuplicateUserException(String message) { + super(message); + } + + /** + * Constructs a new DuplicateUserException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause + */ + public DuplicateUserException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/UserNotFoundException.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/UserNotFoundException.java new file mode 100644 index 00000000..255160ee --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/UserNotFoundException.java @@ -0,0 +1,26 @@ +package com.example.usermanagement.utils; + +/** + * Exception thrown when a user is not found in the system. + */ +public class UserNotFoundException extends RuntimeException { + + /** + * Constructs a new UserNotFoundException with the specified detail message. + * + * @param message the detail message + */ + public UserNotFoundException(String message) { + super(message); + } + + /** + * Constructs a new UserNotFoundException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause + */ + public UserNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/ValidationUtils.java b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/ValidationUtils.java new file mode 100644 index 00000000..4b2d51f2 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/java/user-management/src/main/java/com/example/usermanagement/utils/ValidationUtils.java @@ -0,0 +1,78 @@ +package com.example.usermanagement.utils; + +import org.apache.commons.lang3.StringUtils; + +/** + * Utility class for validation operations. + */ +public final class ValidationUtils { + + private static final String EMAIL_PATTERN = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; + private static final String USERNAME_PATTERN = "^[a-zA-Z0-9_]+$"; + + /** + * Private constructor to prevent instantiation. + */ + private ValidationUtils() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + + /** + * Validates email format. + * + * @param email The email to validate + * @throws IllegalArgumentException if email is invalid + */ + public static void validateEmail(String email) { + if (StringUtils.isBlank(email)) { + throw new IllegalArgumentException("Email cannot be null or empty"); + } + + if (!email.matches(EMAIL_PATTERN)) { + throw new IllegalArgumentException("Invalid email format"); + } + } + + /** + * Validates username format. + * + * @param username The username to validate + * @throws IllegalArgumentException if username is invalid + */ + public static void validateUsername(String username) { + if (StringUtils.isBlank(username)) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + + if (username.length() < 3 || username.length() > 20) { + throw new IllegalArgumentException("Username must be between 3 and 20 characters"); + } + + if (!username.matches(USERNAME_PATTERN)) { + throw new IllegalArgumentException("Username can only contain letters, numbers, and underscores"); + } + } + + /** + * Checks if email format is valid. + * + * @param email The email to check + * @return true if email is valid + */ + public static boolean isValidEmail(String email) { + return StringUtils.isNotBlank(email) && email.matches(EMAIL_PATTERN); + } + + /** + * Checks if username format is valid. + * + * @param username The username to check + * @return true if username is valid + */ + public static boolean isValidUsername(String username) { + return StringUtils.isNotBlank(username) && + username.length() >= 3 && + username.length() <= 20 && + username.matches(USERNAME_PATTERN); + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/.env.example b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/.env.example new file mode 100644 index 00000000..61ea53db --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/.env.example @@ -0,0 +1,29 @@ +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Database Configuration +MONGODB_URI=mongodb://localhost:27017/user-management + +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-here +JWT_EXPIRES_IN=24h + +# CORS Configuration +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001 + +# Logging Configuration +LOG_LEVEL=info + +# Rate Limiting Configuration +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 + +# Password Configuration +BCRYPT_SALT_ROUNDS=12 + +# Email Configuration (if implementing email features) +# SMTP_HOST=smtp.gmail.com +# SMTP_PORT=587 +# SMTP_USER=your-email@gmail.com +# SMTP_PASS=your-app-password \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/README.md b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/README.md new file mode 100644 index 00000000..01c6dbc4 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/README.md @@ -0,0 +1,313 @@ +# User Management System + +A comprehensive user management system built with Node.js, Express, and MongoDB. This project demonstrates enterprise-level patterns for user authentication, authorization, and management. + +## Features + +### Core Functionality +- **User Registration & Authentication**: Secure user registration with JWT-based authentication +- **Role-Based Access Control (RBAC)**: Admin, User, and Guest roles with permission system +- **Password Security**: BCrypt hashing with configurable salt rounds +- **Account Management**: User activation, deactivation, suspension, and soft deletion +- **Profile Management**: User profile updates with validation +- **Permission System**: Granular permissions for fine-grained access control + +### Security Features +- **Rate Limiting**: Configurable rate limits for different endpoints +- **Input Validation**: Comprehensive validation using express-validator +- **Security Headers**: Helmet.js for security headers +- **CORS Protection**: Configurable CORS policies +- **Account Lockout**: Automatic account lockout after failed login attempts +- **JWT Security**: Secure token generation and validation + +### API Features +- **RESTful API**: Clean REST API design with proper HTTP methods +- **Pagination**: Efficient pagination for large datasets +- **Search Functionality**: Full-text search across user fields +- **Filtering**: Role-based and status-based filtering +- **Export Functionality**: User data export capabilities +- **Statistics**: User statistics and analytics + +### Development Features +- **Error Handling**: Comprehensive error handling with custom error classes +- **Logging**: Structured logging with Winston +- **Documentation**: Detailed API documentation +- **Testing**: Unit and integration tests with Jest +- **Code Quality**: ESLint and Prettier configuration + +## Technology Stack + +- **Runtime**: Node.js 16+ +- **Framework**: Express.js +- **Database**: MongoDB with Mongoose ODM +- **Authentication**: JSON Web Tokens (JWT) +- **Password hashing**: BCrypt +- **Validation**: Joi and express-validator +- **Logging**: Winston +- **Testing**: Jest and Supertest +- **Security**: Helmet, CORS, Rate limiting + +## Installation + +### Prerequisites +- Node.js (v16 or higher) +- MongoDB (v4.4 or higher) +- npm or yarn + +### Setup + +1. **Clone the repository** + ```bash + git clone + cd user-management + ``` + +2. **Install dependencies** + ```bash + npm install + ``` + +3. **Environment configuration** + ```bash + cp .env.example .env + ``` + Update the `.env` file with your configuration: + ```env + PORT=3000 + NODE_ENV=development + MONGODB_URI=mongodb://localhost:27017/user-management + JWT_SECRET=your-super-secret-jwt-key-here + JWT_EXPIRES_IN=24h + ``` + +4. **Start MongoDB** + ```bash + # Using MongoDB service + sudo systemctl start mongod + + # Or using Docker + docker run -d -p 27017:27017 --name mongodb mongo:latest + ``` + +5. **Run the application** + ```bash + # Development mode + npm run dev + + # Production mode + npm start + ``` + +## API Documentation + +### Base URL +``` +http://localhost:3000/api +``` + +### Authentication +Most endpoints require authentication. Include the JWT token in the Authorization header: +``` +Authorization: Bearer +``` + +### Endpoints + +#### User Management +- `POST /users` - Create new user +- `GET /users` - Get all users (with pagination) +- `GET /users/:id` - Get user by ID +- `PUT /users/:id` - Update user +- `DELETE /users/:id` - Delete user (soft delete) +- `DELETE /users/:id/hard` - Permanently delete user + +#### Authentication +- `POST /users/auth` - User login +- `PUT /users/:id/password` - Change password +- `PUT /users/:id/reset-password` - Reset password (admin only) + +#### Search & Filtering +- `GET /users/search?q=query` - Search users +- `GET /users/active` - Get active users +- `GET /users/role/:role` - Get users by role + +#### Permissions +- `PUT /users/:id/permissions` - Add permission +- `DELETE /users/:id/permissions` - Remove permission + +#### Analytics +- `GET /users/stats` - Get user statistics +- `GET /users/export` - Export user data +- `GET /users/:id/activity` - Get user activity + +### Example Requests + +#### Create User +```bash +curl -X POST http://localhost:3000/api/users \ + -H "Content-Type: application/json" \ + -d '{ + "username": "john_doe", + "name": "John Doe", + "email": "john@example.com", + "password": "securepassword123", + "age": 30, + "role": "user" + }' +``` + +#### Authenticate User +```bash +curl -X POST http://localhost:3000/api/users/auth \ + -H "Content-Type: application/json" \ + -d '{ + "username": "john_doe", + "password": "securepassword123" + }' +``` + +#### Get Users (with authentication) +```bash +curl -X GET http://localhost:3000/api/users \ + -H "Authorization: Bearer " +``` + +## Testing + +### Run Tests +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage +``` + +### Test Structure +``` +tests/ +├── unit/ +│ ├── models/ +│ ├── services/ +│ └── utils/ +├── integration/ +│ └── routes/ +└── setup/ + └── testSetup.js +``` + +## Development + +### Code Quality +```bash +# Linting +npm run lint + +# Formatting +npm run format +``` + +### Project Structure +``` +src/ +├── config/ +│ └── database.js +├── middleware/ +│ ├── auth.js +│ ├── rateLimiter.js +│ └── validate.js +├── models/ +│ └── User.js +├── routes/ +│ └── userRoutes.js +├── services/ +│ └── UserService.js +├── utils/ +│ ├── errors.js +│ └── logger.js +└── server.js +``` + +### Database Schema + +#### User Schema +```javascript +{ + id: String, // UUID + username: String, // Unique, 3-20 chars + email: String, // Optional, unique + name: String, // Required, 1-100 chars + age: Number, // Optional, 0-150 + password: String, // Hashed, min 8 chars + role: String, // admin, user, guest + status: String, // active, inactive, suspended, deleted + lastLogin: Date, // Last login timestamp + loginAttempts: Number, // Failed login counter + permissions: [String], // Array of permissions + metadata: Object, // Flexible metadata + createdAt: Date, // Auto-generated + updatedAt: Date // Auto-generated +} +``` + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|-------| +| `PORT` | Server port | 3000 | +| `NODE_ENV` | Environment | development | +| `MONGODB_URI` | MongoDB connection string | mongodb://localhost:27017/user-management | +| `JWT_SECRET` | JWT secret key | Required | +| `JWT_EXPIRES_IN` | JWT expiration time | 24h | +| `ALLOWED_ORIGINS` | CORS allowed origins | http://localhost:3000 | +| `LOG_LEVEL` | Logging level | info | +| `BCRYPT_SALT_ROUNDS` | BCrypt salt rounds | 12 | + +## Security Considerations + +1. **Environment Variables**: Never commit sensitive data to version control +2. **JWT Secret**: Use a strong, random JWT secret in production +3. **Rate Limiting**: Adjust rate limits based on your requirements +4. **Input Validation**: All inputs are validated and sanitized +5. **Password Security**: Passwords are hashed using BCrypt with salt rounds +6. **Account Lockout**: Accounts are locked after 5 failed login attempts +7. **CORS**: Configure CORS origins for production +8. **Security Headers**: Helmet.js provides security headers + +## Performance Optimizations + +1. **Database Indexing**: Indexes on frequently queried fields +2. **Pagination**: Efficient pagination for large datasets +3. **Connection Pooling**: MongoDB connection pooling +4. **Compression**: Gzip compression for responses +5. **Caching**: Ready for Redis integration + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests for new functionality +5. Ensure all tests pass +6. Submit a pull request + +## License + +MIT License - see the [LICENSE](LICENSE) file for details. + +## Support + +For support, please open an issue in the GitHub repository or contact the development team. + +## Changelog + +### v1.0.0 +- Initial release +- User management functionality +- Authentication and authorization +- API endpoints +- Security features +- Testing suite \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/package.json b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/package.json new file mode 100644 index 00000000..3d93e39f --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/package.json @@ -0,0 +1,85 @@ +{ + "name": "user-management", + "version": "1.0.0", + "description": "A comprehensive user management system for testing Code Index MCP", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src/ --fix", + "format": "prettier --write src/" + }, + "keywords": [ + "user-management", + "nodejs", + "express", + "authentication", + "api" + ], + "author": "Test Author", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mongoose": "^7.4.1", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.1", + "joi": "^17.9.2", + "cors": "^2.8.5", + "helmet": "^7.0.0", + "express-rate-limit": "^6.8.1", + "winston": "^3.10.0", + "dotenv": "^16.3.1", + "uuid": "^9.0.0", + "morgan": "^1.10.0", + "compression": "^1.7.4", + "express-validator": "^7.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1", + "jest": "^29.6.1", + "supertest": "^6.3.3", + "eslint": "^8.45.0", + "prettier": "^3.0.0", + "mongodb-memory-server": "^8.14.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "jest": { + "testEnvironment": "node", + "coverageDirectory": "coverage", + "collectCoverageFrom": [ + "src/**/*.js", + "!src/server.js" + ] + }, + "eslintConfig": { + "env": { + "node": true, + "es2021": true, + "jest": true + }, + "extends": [ + "eslint:recommended" + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "no-console": "warn", + "no-unused-vars": "error", + "prefer-const": "error", + "no-var": "error" + } + }, + "prettier": { + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/config/database.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/config/database.js new file mode 100644 index 00000000..796796a3 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/config/database.js @@ -0,0 +1,138 @@ +const mongoose = require('mongoose'); +const logger = require('../utils/logger'); + +/** + * Database connection configuration + */ +class Database { + constructor() { + this.mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/user-management'; + this.options = { + useNewUrlParser: true, + useUnifiedTopology: true, + maxPoolSize: 10, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 45000, + family: 4, + }; + } + + /** + * Connect to MongoDB + */ + async connect() { + try { + await mongoose.connect(this.mongoURI, this.options); + logger.info('MongoDB connected successfully'); + + // Handle connection events + mongoose.connection.on('error', (err) => { + logger.error('MongoDB connection error:', err); + }); + + mongoose.connection.on('disconnected', () => { + logger.warn('MongoDB disconnected'); + }); + + mongoose.connection.on('reconnected', () => { + logger.info('MongoDB reconnected'); + }); + + // Handle process termination + process.on('SIGINT', this.gracefulShutdown.bind(this)); + process.on('SIGTERM', this.gracefulShutdown.bind(this)); + + } catch (error) { + logger.error('MongoDB connection failed:', error); + process.exit(1); + } + } + + /** + * Disconnect from MongoDB + */ + async disconnect() { + try { + await mongoose.disconnect(); + logger.info('MongoDB disconnected successfully'); + } catch (error) { + logger.error('MongoDB disconnection error:', error); + } + } + + /** + * Graceful shutdown + */ + async gracefulShutdown(signal) { + logger.info(`Received ${signal}. Graceful shutdown...`); + try { + await this.disconnect(); + process.exit(0); + } catch (error) { + logger.error('Error during graceful shutdown:', error); + process.exit(1); + } + } + + /** + * Get connection status + */ + getConnectionStatus() { + const states = { + 0: 'disconnected', + 1: 'connected', + 2: 'connecting', + 3: 'disconnecting', + }; + return states[mongoose.connection.readyState] || 'unknown'; + } + + /** + * Check if database is connected + */ + isConnected() { + return mongoose.connection.readyState === 1; + } + + /** + * Drop database (for testing) + */ + async dropDatabase() { + if (process.env.NODE_ENV === 'test') { + try { + await mongoose.connection.db.dropDatabase(); + logger.info('Test database dropped'); + } catch (error) { + logger.error('Error dropping test database:', error); + } + } else { + logger.warn('Database drop attempted in non-test environment'); + } + } + + /** + * Get database statistics + */ + async getStats() { + try { + const stats = await mongoose.connection.db.stats(); + return { + database: mongoose.connection.name, + collections: stats.collections, + dataSize: stats.dataSize, + storageSize: stats.storageSize, + indexes: stats.indexes, + indexSize: stats.indexSize, + objects: stats.objects, + }; + } catch (error) { + logger.error('Error getting database stats:', error); + return null; + } + } +} + +// Create singleton instance +const database = new Database(); + +module.exports = database; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/auth.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/auth.js new file mode 100644 index 00000000..a8773e6d --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/auth.js @@ -0,0 +1,165 @@ +const jwt = require('jsonwebtoken'); +const { User } = require('../models/User'); +const { AuthenticationError, AuthorizationError } = require('../utils/errors'); +const logger = require('../utils/logger'); + +/** + * Authentication middleware + * Verifies JWT token and attaches user to request object + */ +const auth = async (req, res, next) => { + try { + // Get token from header + const authHeader = req.header('Authorization'); + const token = authHeader && authHeader.startsWith('Bearer ') + ? authHeader.substring(7) + : null; + + if (!token) { + throw new AuthenticationError('Access denied. No token provided.'); + } + + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret'); + + // Get user from database + const user = await User.findOne({ id: decoded.id }); + if (!user) { + throw new AuthenticationError('Invalid token. User not found.'); + } + + // Check if user is active + if (!user.isActive) { + throw new AuthenticationError('User account is not active.'); + } + + // Attach user to request object + req.user = user; + next(); + } catch (error) { + if (error.name === 'JsonWebTokenError') { + logger.warn('Invalid JWT token attempted'); + next(new AuthenticationError('Invalid token')); + } else if (error.name === 'TokenExpiredError') { + logger.warn('Expired JWT token attempted'); + next(new AuthenticationError('Token expired')); + } else { + logger.error('Authentication error:', error); + next(error); + } + } +}; + +/** + * Authorization middleware factory + * Creates middleware that checks if user has required role + */ +const authorize = (roles) => { + return (req, res, next) => { + if (!req.user) { + return next(new AuthenticationError('Authentication required')); + } + + // Convert single role to array + const allowedRoles = Array.isArray(roles) ? roles : [roles]; + + // Check if user has required role + if (!allowedRoles.includes(req.user.role)) { + logger.warn(`User ${req.user.username} attempted to access resource requiring roles: ${allowedRoles.join(', ')}`); + return next(new AuthorizationError('Insufficient permissions')); + } + + next(); + }; +}; + +/** + * Permission-based authorization middleware + * Checks if user has specific permission + */ +const requirePermission = (permission) => { + return (req, res, next) => { + if (!req.user) { + return next(new AuthenticationError('Authentication required')); + } + + if (!req.user.hasPermission(permission)) { + logger.warn(`User ${req.user.username} attempted to access resource requiring permission: ${permission}`); + return next(new AuthorizationError('Insufficient permissions')); + } + + next(); + }; +}; + +/** + * Self or admin middleware + * Allows access if user is accessing their own data or is an admin + */ +const selfOrAdmin = (req, res, next) => { + if (!req.user) { + return next(new AuthenticationError('Authentication required')); + } + + const targetUserId = req.params.id; + const isAdmin = req.user.role === 'admin'; + const isSelf = req.user.id === targetUserId; + + if (!isAdmin && !isSelf) { + logger.warn(`User ${req.user.username} attempted to access another user's data`); + return next(new AuthorizationError('Access denied')); + } + + next(); +}; + +/** + * Admin only middleware + * Allows access only for admin users + */ +const adminOnly = authorize(['admin']); + +/** + * User or admin middleware + * Allows access for user role and above + */ +const userOrAdmin = authorize(['user', 'admin']); + +/** + * Optional authentication middleware + * Authenticates user if token is provided, but doesn't require it + */ +const optionalAuth = async (req, res, next) => { + try { + const authHeader = req.header('Authorization'); + const token = authHeader && authHeader.startsWith('Bearer ') + ? authHeader.substring(7) + : null; + + if (!token) { + return next(); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret'); + const user = await User.findOne({ id: decoded.id }); + + if (user && user.isActive) { + req.user = user; + } + + next(); + } catch (error) { + // Don't fail on optional auth, just continue without user + next(); + } +}; + +module.exports = { + auth, + authorize, + requirePermission, + selfOrAdmin, + adminOnly, + userOrAdmin, + optionalAuth, +}; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/rateLimiter.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/rateLimiter.js new file mode 100644 index 00000000..bc94d400 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/rateLimiter.js @@ -0,0 +1,122 @@ +const rateLimit = require('express-rate-limit'); +const { RateLimitError } = require('../utils/errors'); +const logger = require('../utils/logger'); + +/** + * General rate limiter + * Limits requests per IP address + */ +const generalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: { + error: { + message: 'Too many requests from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + handler: (req, res) => { + logger.warn(`Rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many requests from this IP, please try again later.'); + res.status(429).json({ + success: false, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }); + }, +}); + +/** + * Authentication rate limiter + * Stricter limits for login attempts + */ +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 login attempts per windowMs + message: { + error: { + message: 'Too many login attempts from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req, res) => { + logger.warn(`Authentication rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many login attempts from this IP, please try again later.'); + res.status(429).json({ + success: false, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }); + }, +}); + +/** + * User creation rate limiter + * Moderate limits for user registration + */ +const createUserLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 10, // limit each IP to 10 user creations per hour + message: { + error: { + message: 'Too many accounts created from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req, res) => { + logger.warn(`User creation rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many accounts created from this IP, please try again later.'); + res.status(429).json({ + success: false, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }); + }, +}); + +/** + * Password reset rate limiter + * Limits password reset attempts + */ +const passwordResetLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 3, // limit each IP to 3 password reset attempts per hour + message: { + error: { + message: 'Too many password reset attempts from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req, res) => { + logger.warn(`Password reset rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many password reset attempts from this IP, please try again later.'); + res.status(429).json({ + success: false, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }); + }, +}); + +module.exports = { + generalLimiter, + authLimiter, + createUserLimiter, + passwordResetLimiter, +}; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/validate.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/validate.js new file mode 100644 index 00000000..87659fa3 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/middleware/validate.js @@ -0,0 +1,29 @@ +const { validationResult } = require('express-validator'); +const { ValidationError } = require('../utils/errors'); + +/** + * Validation middleware + * Checks for validation errors and returns appropriate error response + */ +const validate = (req, res, next) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + const errorMessages = errors.array().map(error => ({ + field: error.path, + message: error.msg, + value: error.value, + })); + + const validationError = new ValidationError( + 'Validation failed', + errorMessages + ); + + return next(validationError); + } + + next(); +}; + +module.exports = validate; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/models/User.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/models/User.js new file mode 100644 index 00000000..d0e5e6c5 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/models/User.js @@ -0,0 +1,333 @@ +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const { v4: uuidv4 } = require('uuid'); + +// User roles enumeration +const USER_ROLES = { + ADMIN: 'admin', + USER: 'user', + GUEST: 'guest', +}; + +// User status enumeration +const USER_STATUS = { + ACTIVE: 'active', + INACTIVE: 'inactive', + SUSPENDED: 'suspended', + DELETED: 'deleted', +}; + +// User schema definition +const userSchema = new mongoose.Schema( + { + id: { + type: String, + default: uuidv4, + unique: true, + required: true, + }, + username: { + type: String, + required: [true, 'Username is required'], + unique: true, + minlength: [3, 'Username must be at least 3 characters'], + maxlength: [20, 'Username cannot exceed 20 characters'], + match: [/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'], + }, + email: { + type: String, + unique: true, + sparse: true, + match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email'], + }, + name: { + type: String, + required: [true, 'Name is required'], + minlength: [1, 'Name is required'], + maxlength: [100, 'Name cannot exceed 100 characters'], + }, + age: { + type: Number, + min: [0, 'Age cannot be negative'], + max: [150, 'Age cannot exceed 150'], + }, + password: { + type: String, + required: [true, 'Password is required'], + minlength: [8, 'Password must be at least 8 characters'], + select: false, // Don't include in queries by default + }, + role: { + type: String, + enum: Object.values(USER_ROLES), + default: USER_ROLES.USER, + }, + status: { + type: String, + enum: Object.values(USER_STATUS), + default: USER_STATUS.ACTIVE, + }, + lastLogin: { + type: Date, + default: null, + }, + loginAttempts: { + type: Number, + default: 0, + }, + permissions: { + type: [String], + default: [], + }, + metadata: { + type: mongoose.Schema.Types.Mixed, + default: {}, + }, + }, + { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true }, + } +); + +// Virtual for user response (without sensitive data) +userSchema.virtual('response').get(function() { + return { + id: this.id, + username: this.username, + email: this.email, + name: this.name, + age: this.age, + role: this.role, + status: this.status, + lastLogin: this.lastLogin, + permissions: this.permissions, + metadata: this.metadata, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; +}); + +// Virtual for checking if user is active +userSchema.virtual('isActive').get(function() { + return this.status === USER_STATUS.ACTIVE; +}); + +// Virtual for checking if user is admin +userSchema.virtual('isAdmin').get(function() { + return this.role === USER_ROLES.ADMIN; +}); + +// Virtual for checking if user is locked +userSchema.virtual('isLocked').get(function() { + return this.loginAttempts >= 5 || this.status === USER_STATUS.SUSPENDED; +}); + +// Pre-save middleware to hash password +userSchema.pre('save', async function(next) { + // Only hash the password if it has been modified (or is new) + if (!this.isModified('password')) return next(); + + try { + // Hash password with cost of 12 + const hashedPassword = await bcrypt.hash(this.password, 12); + this.password = hashedPassword; + next(); + } catch (error) { + next(error); + } +}); + +// Instance method to check password +userSchema.methods.checkPassword = async function(candidatePassword) { + return await bcrypt.compare(candidatePassword, this.password); +}; + +// Instance method to generate JWT token +userSchema.methods.generateToken = function() { + return jwt.sign( + { + id: this.id, + username: this.username, + role: this.role + }, + process.env.JWT_SECRET || 'fallback-secret', + { + expiresIn: process.env.JWT_EXPIRES_IN || '24h', + issuer: 'user-management-system' + } + ); +}; + +// Instance method to add permission +userSchema.methods.addPermission = function(permission) { + if (!this.permissions.includes(permission)) { + this.permissions.push(permission); + } +}; + +// Instance method to remove permission +userSchema.methods.removePermission = function(permission) { + this.permissions = this.permissions.filter(p => p !== permission); +}; + +// Instance method to check permission +userSchema.methods.hasPermission = function(permission) { + return this.permissions.includes(permission); +}; + +// Instance method to record successful login +userSchema.methods.recordLogin = function() { + this.lastLogin = new Date(); + this.loginAttempts = 0; +}; + +// Instance method to record failed login attempt +userSchema.methods.recordFailedLogin = function() { + this.loginAttempts += 1; + if (this.loginAttempts >= 5) { + this.status = USER_STATUS.SUSPENDED; + } +}; + +// Instance method to reset login attempts +userSchema.methods.resetLoginAttempts = function() { + this.loginAttempts = 0; +}; + +// Instance method to activate user +userSchema.methods.activate = function() { + this.status = USER_STATUS.ACTIVE; + this.loginAttempts = 0; +}; + +// Instance method to deactivate user +userSchema.methods.deactivate = function() { + this.status = USER_STATUS.INACTIVE; +}; + +// Instance method to suspend user +userSchema.methods.suspend = function() { + this.status = USER_STATUS.SUSPENDED; +}; + +// Instance method to delete user (soft delete) +userSchema.methods.delete = function() { + this.status = USER_STATUS.DELETED; +}; + +// Instance method to get metadata +userSchema.methods.getMetadata = function(key, defaultValue = null) { + return this.metadata[key] || defaultValue; +}; + +// Instance method to set metadata +userSchema.methods.setMetadata = function(key, value) { + this.metadata[key] = value; +}; + +// Instance method to remove metadata +userSchema.methods.removeMetadata = function(key) { + delete this.metadata[key]; +}; + +// Instance method to validate user data +userSchema.methods.validateUser = function() { + const errors = []; + + if (!this.username || this.username.length < 3) { + errors.push('Username must be at least 3 characters'); + } + + if (!this.name || this.name.length === 0) { + errors.push('Name is required'); + } + + if (this.age && (this.age < 0 || this.age > 150)) { + errors.push('Age must be between 0 and 150'); + } + + if (this.email && !this.email.match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/)) { + errors.push('Email format is invalid'); + } + + return errors; +}; + +// Static method to find by username +userSchema.statics.findByUsername = function(username) { + return this.findOne({ username }); +}; + +// Static method to find by email +userSchema.statics.findByEmail = function(email) { + return this.findOne({ email }); +}; + +// Static method to find active users +userSchema.statics.findActive = function() { + return this.find({ status: USER_STATUS.ACTIVE }); +}; + +// Static method to find by role +userSchema.statics.findByRole = function(role) { + return this.find({ role }); +}; + +// Static method to search users +userSchema.statics.searchUsers = function(query, options = {}) { + const searchRegex = new RegExp(query, 'i'); + const searchQuery = { + $or: [ + { username: searchRegex }, + { name: searchRegex }, + { email: searchRegex }, + ], + }; + + return this.find(searchQuery, null, options); +}; + +// Static method to get user statistics +userSchema.statics.getUserStats = async function() { + const stats = await this.aggregate([ + { + $group: { + _id: null, + total: { $sum: 1 }, + active: { $sum: { $cond: [{ $eq: ['$status', USER_STATUS.ACTIVE] }, 1, 0] } }, + admin: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.ADMIN] }, 1, 0] } }, + user: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.USER] }, 1, 0] } }, + guest: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.GUEST] }, 1, 0] } }, + withEmail: { $sum: { $cond: [{ $ne: ['$email', null] }, 1, 0] } }, + }, + }, + ]); + + return stats[0] || { + total: 0, + active: 0, + admin: 0, + user: 0, + guest: 0, + withEmail: 0, + }; +}; + +// Index for performance +userSchema.index({ username: 1 }); +userSchema.index({ email: 1 }); +userSchema.index({ role: 1 }); +userSchema.index({ status: 1 }); +userSchema.index({ createdAt: -1 }); + +// Export model and constants +const User = mongoose.model('User', userSchema); + +module.exports = { + User, + USER_ROLES, + USER_STATUS, +}; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/routes/userRoutes.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/routes/userRoutes.js new file mode 100644 index 00000000..2c2d4dd8 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/routes/userRoutes.js @@ -0,0 +1,268 @@ +const express = require('express'); +const { body, param, query } = require('express-validator'); +const UserService = require('../services/UserService'); +const { asyncHandler, createSuccessResponse, createErrorResponse } = require('../utils/errors'); +const auth = require('../middleware/auth'); +const validate = require('../middleware/validate'); +const logger = require('../utils/logger'); + +const router = express.Router(); +const userService = new UserService(); + +// User creation validation +const createUserValidation = [ + body('username') + .isLength({ min: 3, max: 20 }) + .withMessage('Username must be between 3 and 20 characters') + .matches(/^[a-zA-Z0-9_]+$/) + .withMessage('Username can only contain letters, numbers, and underscores'), + body('name') + .isLength({ min: 1, max: 100 }) + .withMessage('Name must be between 1 and 100 characters'), + body('email') + .optional() + .isEmail() + .withMessage('Please provide a valid email address'), + body('age') + .optional() + .isInt({ min: 0, max: 150 }) + .withMessage('Age must be between 0 and 150'), + body('password') + .isLength({ min: 8 }) + .withMessage('Password must be at least 8 characters long'), + body('role') + .optional() + .isIn(['admin', 'user', 'guest']) + .withMessage('Role must be admin, user, or guest'), +]; + +// User update validation +const updateUserValidation = [ + param('id').notEmpty().withMessage('User ID is required'), + body('username') + .optional() + .isLength({ min: 3, max: 20 }) + .withMessage('Username must be between 3 and 20 characters') + .matches(/^[a-zA-Z0-9_]+$/) + .withMessage('Username can only contain letters, numbers, and underscores'), + body('name') + .optional() + .isLength({ min: 1, max: 100 }) + .withMessage('Name must be between 1 and 100 characters'), + body('email') + .optional() + .isEmail() + .withMessage('Please provide a valid email address'), + body('age') + .optional() + .isInt({ min: 0, max: 150 }) + .withMessage('Age must be between 0 and 150'), + body('role') + .optional() + .isIn(['admin', 'user', 'guest']) + .withMessage('Role must be admin, user, or guest'), +]; + +// Password change validation +const passwordChangeValidation = [ + param('id').notEmpty().withMessage('User ID is required'), + body('currentPassword') + .isLength({ min: 1 }) + .withMessage('Current password is required'), + body('newPassword') + .isLength({ min: 8 }) + .withMessage('New password must be at least 8 characters long'), +]; + +// Authentication validation +const authValidation = [ + body('username') + .isLength({ min: 3 }) + .withMessage('Username must be at least 3 characters'), + body('password') + .isLength({ min: 1 }) + .withMessage('Password is required'), +]; + +// Search validation +const searchValidation = [ + query('q') + .isLength({ min: 1 }) + .withMessage('Search query is required'), + query('page') + .optional() + .isInt({ min: 1 }) + .withMessage('Page must be a positive integer'), + query('limit') + .optional() + .isInt({ min: 1, max: 100 }) + .withMessage('Limit must be between 1 and 100'), +]; + +// @route POST /api/users +// @desc Create a new user +// @access Public +router.post('/', createUserValidation, validate, asyncHandler(async (req, res) => { + const user = await userService.createUser(req.body); + logger.info(`User created via API: ${user.username}`); + res.status(201).json(createSuccessResponse(user, 'User created successfully')); +})); + +// @route POST /api/users/auth +// @desc Authenticate user +// @access Public +router.post('/auth', authValidation, validate, asyncHandler(async (req, res) => { + const { username, password } = req.body; + const result = await userService.authenticateUser(username, password); + logger.info(`User authenticated via API: ${username}`); + res.json(createSuccessResponse(result, 'Authentication successful')); +})); + +// @route GET /api/users +// @desc Get all users with pagination +// @access Private (Admin only) +router.get('/', auth, asyncHandler(async (req, res) => { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + const filter = {}; + + // Add filtering by role if provided + if (req.query.role) { + filter.role = req.query.role; + } + + // Add filtering by status if provided + if (req.query.status) { + filter.status = req.query.status; + } + + const result = await userService.getAllUsers(page, limit, filter); + res.json(createSuccessResponse(result, 'Users retrieved successfully')); +})); + +// @route GET /api/users/search +// @desc Search users +// @access Private +router.get('/search', auth, searchValidation, validate, asyncHandler(async (req, res) => { + const { q: query, page = 1, limit = 20 } = req.query; + const result = await userService.searchUsers(query, parseInt(page), parseInt(limit)); + res.json(createSuccessResponse(result, 'Search completed successfully')); +})); + +// @route GET /api/users/stats +// @desc Get user statistics +// @access Private (Admin only) +router.get('/stats', auth, asyncHandler(async (req, res) => { + const stats = await userService.getUserStats(); + res.json(createSuccessResponse(stats, 'Statistics retrieved successfully')); +})); + +// @route GET /api/users/export +// @desc Export all users +// @access Private (Admin only) +router.get('/export', auth, asyncHandler(async (req, res) => { + const users = await userService.exportUsers(); + res.json(createSuccessResponse(users, 'Users exported successfully')); +})); + +// @route GET /api/users/active +// @desc Get active users +// @access Private +router.get('/active', auth, asyncHandler(async (req, res) => { + const users = await userService.getActiveUsers(); + res.json(createSuccessResponse(users, 'Active users retrieved successfully')); +})); + +// @route GET /api/users/role/:role +// @desc Get users by role +// @access Private (Admin only) +router.get('/role/:role', auth, asyncHandler(async (req, res) => { + const { role } = req.params; + const users = await userService.getUsersByRole(role); + res.json(createSuccessResponse(users, `Users with role ${role} retrieved successfully`)); +})); + +// @route GET /api/users/:id +// @desc Get user by ID +// @access Private +router.get('/:id', auth, asyncHandler(async (req, res) => { + const user = await userService.getUserById(req.params.id); + res.json(createSuccessResponse(user, 'User retrieved successfully')); +})); + +// @route GET /api/users/:id/activity +// @desc Get user activity +// @access Private (Admin or same user) +router.get('/:id/activity', auth, asyncHandler(async (req, res) => { + const activity = await userService.getUserActivity(req.params.id); + res.json(createSuccessResponse(activity, 'User activity retrieved successfully')); +})); + +// @route PUT /api/users/:id +// @desc Update user +// @access Private (Admin or same user) +router.put('/:id', auth, updateUserValidation, validate, asyncHandler(async (req, res) => { + const user = await userService.updateUser(req.params.id, req.body); + logger.info(`User updated via API: ${user.username}`); + res.json(createSuccessResponse(user, 'User updated successfully')); +})); + +// @route PUT /api/users/:id/password +// @desc Change user password +// @access Private (Admin or same user) +router.put('/:id/password', auth, passwordChangeValidation, validate, asyncHandler(async (req, res) => { + const { currentPassword, newPassword } = req.body; + await userService.changePassword(req.params.id, currentPassword, newPassword); + logger.info(`Password changed via API for user: ${req.params.id}`); + res.json(createSuccessResponse(null, 'Password changed successfully')); +})); + +// @route PUT /api/users/:id/reset-password +// @desc Reset user password (Admin only) +// @access Private (Admin only) +router.put('/:id/reset-password', auth, asyncHandler(async (req, res) => { + const { newPassword } = req.body; + await userService.resetPassword(req.params.id, newPassword); + logger.info(`Password reset via API for user: ${req.params.id}`); + res.json(createSuccessResponse(null, 'Password reset successfully')); +})); + +// @route PUT /api/users/:id/permissions +// @desc Add permission to user +// @access Private (Admin only) +router.put('/:id/permissions', auth, asyncHandler(async (req, res) => { + const { permission } = req.body; + await userService.addPermission(req.params.id, permission); + logger.info(`Permission added via API for user: ${req.params.id}`); + res.json(createSuccessResponse(null, 'Permission added successfully')); +})); + +// @route DELETE /api/users/:id/permissions +// @desc Remove permission from user +// @access Private (Admin only) +router.delete('/:id/permissions', auth, asyncHandler(async (req, res) => { + const { permission } = req.body; + await userService.removePermission(req.params.id, permission); + logger.info(`Permission removed via API for user: ${req.params.id}`); + res.json(createSuccessResponse(null, 'Permission removed successfully')); +})); + +// @route DELETE /api/users/:id +// @desc Delete user (soft delete) +// @access Private (Admin only) +router.delete('/:id', auth, asyncHandler(async (req, res) => { + await userService.deleteUser(req.params.id); + logger.info(`User deleted via API: ${req.params.id}`); + res.json(createSuccessResponse(null, 'User deleted successfully')); +})); + +// @route DELETE /api/users/:id/hard +// @desc Hard delete user (permanent) +// @access Private (Admin only) +router.delete('/:id/hard', auth, asyncHandler(async (req, res) => { + await userService.hardDeleteUser(req.params.id); + logger.info(`User permanently deleted via API: ${req.params.id}`); + res.json(createSuccessResponse(null, 'User permanently deleted')); +})); + +module.exports = router; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/server.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/server.js new file mode 100644 index 00000000..0c1f8503 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/server.js @@ -0,0 +1,190 @@ +require('dotenv').config(); + +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const compression = require('compression'); +const morgan = require('morgan'); + +const database = require('./config/database'); +const userRoutes = require('./routes/userRoutes'); +const { generalLimiter } = require('./middleware/rateLimiter'); +const { globalErrorHandler, handleNotFound } = require('./utils/errors'); +const logger = require('./utils/logger'); + +/** + * Express application setup + */ +class App { + constructor() { + this.app = express(); + this.port = process.env.PORT || 3000; + this.setupMiddleware(); + this.setupRoutes(); + this.setupErrorHandling(); + } + + /** + * Setup middleware + */ + setupMiddleware() { + // Security middleware + this.app.use(helmet()); + + // CORS configuration + this.app.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'], + credentials: true, + })); + + // Compression middleware + this.app.use(compression()); + + // Body parsing middleware + this.app.use(express.json({ limit: '10mb' })); + this.app.use(express.urlencoded({ extended: true, limit: '10mb' })); + + // Logging middleware + this.app.use(morgan('combined', { stream: logger.stream })); + + // Rate limiting + this.app.use(generalLimiter); + + // Request ID middleware + this.app.use((req, res, next) => { + req.requestId = Math.random().toString(36).substr(2, 9); + res.set('X-Request-ID', req.requestId); + next(); + }); + + // Health check endpoint + this.app.get('/health', (req, res) => { + res.json({ + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + database: database.getConnectionStatus(), + version: process.env.npm_package_version || '1.0.0', + }); + }); + } + + /** + * Setup routes + */ + setupRoutes() { + // API routes + this.app.use('/api/users', userRoutes); + + // Root endpoint + this.app.get('/', (req, res) => { + res.json({ + message: 'User Management API', + version: '1.0.0', + endpoints: { + health: '/health', + users: '/api/users', + auth: '/api/users/auth', + }, + }); + }); + } + + /** + * Setup error handling + */ + setupErrorHandling() { + // Handle 404 for unknown routes + this.app.use(handleNotFound); + + // Global error handler + this.app.use(globalErrorHandler); + } + + /** + * Start the server + */ + async start() { + try { + // Connect to database + await database.connect(); + + // Start server + this.server = this.app.listen(this.port, () => { + logger.info(`Server running on port ${this.port}`); + logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`); + logger.info(`Health check: http://localhost:${this.port}/health`); + }); + + // Handle server errors + this.server.on('error', (error) => { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof this.port === 'string' ? `Pipe ${this.port}` : `Port ${this.port}`; + + switch (error.code) { + case 'EACCES': + logger.error(`${bind} requires elevated privileges`); + process.exit(1); + break; + case 'EADDRINUSE': + logger.error(`${bind} is already in use`); + process.exit(1); + break; + default: + throw error; + } + }); + + // Graceful shutdown + process.on('SIGTERM', this.gracefulShutdown.bind(this)); + process.on('SIGINT', this.gracefulShutdown.bind(this)); + + } catch (error) { + logger.error('Failed to start server:', error); + process.exit(1); + } + } + + /** + * Graceful shutdown + */ + async gracefulShutdown(signal) { + logger.info(`Received ${signal}. Graceful shutdown...`); + + if (this.server) { + this.server.close(async () => { + logger.info('HTTP server closed'); + + try { + await database.disconnect(); + logger.info('Database disconnected'); + process.exit(0); + } catch (error) { + logger.error('Error during graceful shutdown:', error); + process.exit(1); + } + }); + } + } + + /** + * Get Express app instance + */ + getApp() { + return this.app; + } +} + +// Create and start the application +const app = new App(); + +// Start server if not in test environment +if (process.env.NODE_ENV !== 'test') { + app.start(); +} + +// Export for testing +module.exports = app.getApp(); \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/services/UserService.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/services/UserService.js new file mode 100644 index 00000000..5dfde99a --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/services/UserService.js @@ -0,0 +1,493 @@ +const { User, USER_ROLES, USER_STATUS } = require('../models/User'); +const { AppError } = require('../utils/errors'); +const logger = require('../utils/logger'); + +/** + * UserService class handles all user-related business logic + */ +class UserService { + /** + * Create a new user + * @param {Object} userData - User data object + * @returns {Promise} Created user response + */ + async createUser(userData) { + try { + // Check if username already exists + const existingUsername = await User.findByUsername(userData.username); + if (existingUsername) { + throw new AppError('Username already exists', 400); + } + + // Check if email already exists (if provided) + if (userData.email) { + const existingEmail = await User.findByEmail(userData.email); + if (existingEmail) { + throw new AppError('Email already exists', 400); + } + } + + // Create new user + const user = new User(userData); + + // Validate user data + const validationErrors = user.validateUser(); + if (validationErrors.length > 0) { + throw new AppError(validationErrors.join(', '), 400); + } + + await user.save(); + + logger.info(`User created successfully: ${user.username}`); + return user.response; + } catch (error) { + logger.error('Error creating user:', error); + throw error; + } + } + + /** + * Get user by ID + * @param {string} id - User ID + * @returns {Promise} User response + */ + async getUserById(id) { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + return user.response; + } catch (error) { + logger.error('Error getting user by ID:', error); + throw error; + } + } + + /** + * Get user by username + * @param {string} username - Username + * @returns {Promise} User response + */ + async getUserByUsername(username) { + try { + const user = await User.findByUsername(username); + if (!user) { + throw new AppError('User not found', 404); + } + return user.response; + } catch (error) { + logger.error('Error getting user by username:', error); + throw error; + } + } + + /** + * Get user by email + * @param {string} email - Email address + * @returns {Promise} User response + */ + async getUserByEmail(email) { + try { + const user = await User.findByEmail(email); + if (!user) { + throw new AppError('User not found', 404); + } + return user.response; + } catch (error) { + logger.error('Error getting user by email:', error); + throw error; + } + } + + /** + * Update user + * @param {string} id - User ID + * @param {Object} updateData - Update data + * @returns {Promise} Updated user response + */ + async updateUser(id, updateData) { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + // Apply updates + Object.keys(updateData).forEach(key => { + if (key !== 'password' && key !== 'id') { + user[key] = updateData[key]; + } + }); + + // Validate updated data + const validationErrors = user.validateUser(); + if (validationErrors.length > 0) { + throw new AppError(validationErrors.join(', '), 400); + } + + await user.save(); + + logger.info(`User updated successfully: ${user.username}`); + return user.response; + } catch (error) { + logger.error('Error updating user:', error); + throw error; + } + } + + /** + * Delete user (soft delete) + * @param {string} id - User ID + * @returns {Promise} Success status + */ + async deleteUser(id) { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + user.delete(); + await user.save(); + + logger.info(`User deleted successfully: ${user.username}`); + return true; + } catch (error) { + logger.error('Error deleting user:', error); + throw error; + } + } + + /** + * Hard delete user (permanent deletion) + * @param {string} id - User ID + * @returns {Promise} Success status + */ + async hardDeleteUser(id) { + try { + const result = await User.deleteOne({ id }); + if (result.deletedCount === 0) { + throw new AppError('User not found', 404); + } + + logger.info(`User permanently deleted: ${id}`); + return true; + } catch (error) { + logger.error('Error hard deleting user:', error); + throw error; + } + } + + /** + * Get all users with pagination + * @param {number} page - Page number + * @param {number} limit - Items per page + * @param {Object} filter - Filter criteria + * @returns {Promise} Paginated users response + */ + async getAllUsers(page = 1, limit = 20, filter = {}) { + try { + const skip = (page - 1) * limit; + + const users = await User.find(filter) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit); + + const total = await User.countDocuments(filter); + const totalPages = Math.ceil(total / limit); + + return { + users: users.map(user => user.response), + pagination: { + page, + limit, + total, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1, + }, + }; + } catch (error) { + logger.error('Error getting all users:', error); + throw error; + } + } + + /** + * Get active users + * @returns {Promise} Active users + */ + async getActiveUsers() { + try { + const users = await User.findActive(); + return users.map(user => user.response); + } catch (error) { + logger.error('Error getting active users:', error); + throw error; + } + } + + /** + * Get users by role + * @param {string} role - User role + * @returns {Promise} Users with specified role + */ + async getUsersByRole(role) { + try { + const users = await User.findByRole(role); + return users.map(user => user.response); + } catch (error) { + logger.error('Error getting users by role:', error); + throw error; + } + } + + /** + * Search users + * @param {string} query - Search query + * @param {number} page - Page number + * @param {number} limit - Items per page + * @returns {Promise} Search results + */ + async searchUsers(query, page = 1, limit = 20) { + try { + const skip = (page - 1) * limit; + + const users = await User.searchUsers(query, { + skip, + limit, + sort: { createdAt: -1 }, + }); + + // Count total matching users + const totalUsers = await User.searchUsers(query); + const total = totalUsers.length; + const totalPages = Math.ceil(total / limit); + + return { + users: users.map(user => user.response), + query, + pagination: { + page, + limit, + total, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1, + }, + }; + } catch (error) { + logger.error('Error searching users:', error); + throw error; + } + } + + /** + * Get user statistics + * @returns {Promise} User statistics + */ + async getUserStats() { + try { + const stats = await User.getUserStats(); + return stats; + } catch (error) { + logger.error('Error getting user statistics:', error); + throw error; + } + } + + /** + * Authenticate user + * @param {string} username - Username + * @param {string} password - Password + * @returns {Promise} Authentication result + */ + async authenticateUser(username, password) { + try { + const user = await User.findByUsername(username).select('+password'); + + if (!user || !(await user.checkPassword(password))) { + // Record failed login attempt if user exists + if (user) { + user.recordFailedLogin(); + await user.save(); + } + throw new AppError('Invalid username or password', 401); + } + + if (!user.isActive) { + throw new AppError('User account is not active', 401); + } + + if (user.isLocked) { + throw new AppError('User account is locked', 401); + } + + // Record successful login + user.recordLogin(); + await user.save(); + + // Generate token + const token = user.generateToken(); + + logger.info(`User authenticated successfully: ${user.username}`); + + return { + user: user.response, + token, + }; + } catch (error) { + logger.error('Error authenticating user:', error); + throw error; + } + } + + /** + * Change user password + * @param {string} id - User ID + * @param {string} currentPassword - Current password + * @param {string} newPassword - New password + * @returns {Promise} Success status + */ + async changePassword(id, currentPassword, newPassword) { + try { + const user = await User.findOne({ id }).select('+password'); + if (!user) { + throw new AppError('User not found', 404); + } + + if (!(await user.checkPassword(currentPassword))) { + throw new AppError('Current password is incorrect', 400); + } + + user.password = newPassword; + await user.save(); + + logger.info(`Password changed successfully for user: ${user.username}`); + return true; + } catch (error) { + logger.error('Error changing password:', error); + throw error; + } + } + + /** + * Reset user password (admin function) + * @param {string} id - User ID + * @param {string} newPassword - New password + * @returns {Promise} Success status + */ + async resetPassword(id, newPassword) { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + user.password = newPassword; + user.resetLoginAttempts(); + await user.save(); + + logger.info(`Password reset successfully for user: ${user.username}`); + return true; + } catch (error) { + logger.error('Error resetting password:', error); + throw error; + } + } + + /** + * Add permission to user + * @param {string} id - User ID + * @param {string} permission - Permission to add + * @returns {Promise} Success status + */ + async addPermission(id, permission) { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + user.addPermission(permission); + await user.save(); + + logger.info(`Permission added to user ${user.username}: ${permission}`); + return true; + } catch (error) { + logger.error('Error adding permission:', error); + throw error; + } + } + + /** + * Remove permission from user + * @param {string} id - User ID + * @param {string} permission - Permission to remove + * @returns {Promise} Success status + */ + async removePermission(id, permission) { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + user.removePermission(permission); + await user.save(); + + logger.info(`Permission removed from user ${user.username}: ${permission}`); + return true; + } catch (error) { + logger.error('Error removing permission:', error); + throw error; + } + } + + /** + * Export users data + * @returns {Promise} Users data for export + */ + async exportUsers() { + try { + const users = await User.find().sort({ createdAt: -1 }); + return users.map(user => user.response); + } catch (error) { + logger.error('Error exporting users:', error); + throw error; + } + } + + /** + * Get user activity + * @param {string} id - User ID + * @returns {Promise} User activity data + */ + async getUserActivity(id) { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + return { + id: user.id, + username: user.username, + lastLogin: user.lastLogin, + loginAttempts: user.loginAttempts, + isActive: user.isActive, + isLocked: user.isLocked, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; + } catch (error) { + logger.error('Error getting user activity:', error); + throw error; + } + } +} + +module.exports = UserService; +// AUTO_REINDEX_MARKER: ci_auto_reindex_test_token_js \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/utils/errors.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/utils/errors.js new file mode 100644 index 00000000..15677ac3 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/utils/errors.js @@ -0,0 +1,206 @@ +/** + * Custom error classes for the application + */ + +/** + * Base application error class + */ +class AppError extends Error { + constructor(message, statusCode = 500, isOperational = true) { + super(message); + + this.statusCode = statusCode; + this.isOperational = isOperational; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + + // Capture stack trace + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * Validation error class + */ +class ValidationError extends AppError { + constructor(message, errors = []) { + super(message, 400); + this.errors = errors; + } +} + +/** + * Authentication error class + */ +class AuthenticationError extends AppError { + constructor(message = 'Authentication failed') { + super(message, 401); + } +} + +/** + * Authorization error class + */ +class AuthorizationError extends AppError { + constructor(message = 'Access denied') { + super(message, 403); + } +} + +/** + * Not found error class + */ +class NotFoundError extends AppError { + constructor(message = 'Resource not found') { + super(message, 404); + } +} + +/** + * Conflict error class + */ +class ConflictError extends AppError { + constructor(message = 'Resource conflict') { + super(message, 409); + } +} + +/** + * Rate limit error class + */ +class RateLimitError extends AppError { + constructor(message = 'Too many requests') { + super(message, 429); + } +} + +/** + * Database error class + */ +class DatabaseError extends AppError { + constructor(message = 'Database error') { + super(message, 500); + } +} + +/** + * External service error class + */ +class ExternalServiceError extends AppError { + constructor(message = 'External service error') { + super(message, 502); + } +} + +/** + * Global error handler for Express + */ +const globalErrorHandler = (err, req, res, next) => { + // Default error values + let error = { ...err }; + error.message = err.message; + + // Log error + console.error('Error:', err); + + // Mongoose bad ObjectId + if (err.name === 'CastError') { + const message = 'Resource not found'; + error = new NotFoundError(message); + } + + // Mongoose duplicate key + if (err.code === 11000) { + const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0]; + const message = `Duplicate field value: ${value}. Please use another value`; + error = new ConflictError(message); + } + + // Mongoose validation error + if (err.name === 'ValidationError') { + const errors = Object.values(err.errors).map(val => val.message); + error = new ValidationError('Validation failed', errors); + } + + // JWT errors + if (err.name === 'JsonWebTokenError') { + error = new AuthenticationError('Invalid token'); + } + + if (err.name === 'TokenExpiredError') { + error = new AuthenticationError('Token expired'); + } + + // Send error response + res.status(error.statusCode || 500).json({ + success: false, + error: { + message: error.message, + ...(process.env.NODE_ENV === 'development' && { stack: error.stack }), + ...(error.errors && { errors: error.errors }), + }, + }); +}; + +/** + * Async error handler wrapper + */ +const asyncHandler = (fn) => { + return (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +}; + +/** + * Create error response + */ +const createErrorResponse = (message, statusCode = 500, errors = null) => { + const response = { + success: false, + error: { + message, + statusCode, + }, + }; + + if (errors) { + response.error.errors = errors; + } + + return response; +}; + +/** + * Create success response + */ +const createSuccessResponse = (data, message = 'Success') => { + return { + success: true, + message, + data, + }; +}; + +/** + * Handle 404 for unknown routes + */ +const handleNotFound = (req, res, next) => { + const error = new NotFoundError(`Route ${req.originalUrl} not found`); + next(error); +}; + +module.exports = { + AppError, + ValidationError, + AuthenticationError, + AuthorizationError, + NotFoundError, + ConflictError, + RateLimitError, + DatabaseError, + ExternalServiceError, + globalErrorHandler, + asyncHandler, + createErrorResponse, + createSuccessResponse, + handleNotFound, +}; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/utils/logger.js b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/utils/logger.js new file mode 100644 index 00000000..13c4f24f --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/javascript/user-management/src/utils/logger.js @@ -0,0 +1,79 @@ +const winston = require('winston'); + +// Define log levels +const levels = { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, +}; + +// Define colors for each level +const colors = { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'white', +}; + +// Tell winston about the colors +winston.addColors(colors); + +// Format function +const format = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), + winston.format.colorize({ all: true }), + winston.format.printf( + (info) => `${info.timestamp} ${info.level}: ${info.message}` + ) +); + +// Define which transports the logger must use +const transports = [ + // Console transport + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ), + }), + + // File transport for errors + new winston.transports.File({ + filename: 'logs/error.log', + level: 'error', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }), + + // File transport for all logs + new winston.transports.File({ + filename: 'logs/combined.log', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }), +]; + +// Create the logger +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + levels, + format, + transports, +}); + +// Create a stream object with a 'write' function that will be used by Morgan +logger.stream = { + write: (message) => { + // Remove the trailing newline + logger.http(message.trim()); + }, +}; + +module.exports = logger; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/objective-c/Person.h b/reference/code-index-mcp-master/test/sample-projects/objective-c/Person.h new file mode 100644 index 00000000..368bc379 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/objective-c/Person.h @@ -0,0 +1,14 @@ +#import + +@interface Person : NSObject + +@property (nonatomic, strong) NSString *name; +@property (nonatomic, assign) NSInteger age; +@property (nonatomic, strong) NSString *email; + +- (instancetype)initWithName:(NSString *)name age:(NSInteger)age; +- (void)sayHello; +- (void)updateEmail:(NSString *)email; ++ (Person *)createDefaultPerson; + +@end \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/objective-c/Person.m b/reference/code-index-mcp-master/test/sample-projects/objective-c/Person.m new file mode 100644 index 00000000..78a744a4 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/objective-c/Person.m @@ -0,0 +1,27 @@ +#import "Person.h" + +@implementation Person + +- (instancetype)initWithName:(NSString *)name age:(NSInteger)age { + self = [super init]; + if (self) { + _name = name; + _age = age; + } + return self; +} + +- (void)sayHello { + NSLog(@"Hello, my name is %@", self.name); +} + +- (void)updateEmail:(NSString *)email { + self.email = email; + NSLog(@"Email updated to: %@", email); +} + ++ (Person *)createDefaultPerson { + return [[Person alloc] initWithName:@"John Doe" age:30]; +} + +@end \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/objective-c/UserManager.h b/reference/code-index-mcp-master/test/sample-projects/objective-c/UserManager.h new file mode 100644 index 00000000..d08c4d81 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/objective-c/UserManager.h @@ -0,0 +1,14 @@ +#import +#import "Person.h" + +@interface UserManager : NSObject + +@property (nonatomic, strong) NSMutableArray *users; + ++ (UserManager *)sharedManager; +- (void)addUser:(Person *)user; +- (Person *)findUserByName:(NSString *)name; +- (void)removeUser:(Person *)user; +- (NSInteger)userCount; + +@end \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/objective-c/UserManager.m b/reference/code-index-mcp-master/test/sample-projects/objective-c/UserManager.m new file mode 100644 index 00000000..1157d7c7 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/objective-c/UserManager.m @@ -0,0 +1,42 @@ +#import "UserManager.h" + +@implementation UserManager + ++ (UserManager *)sharedManager { + static UserManager *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[UserManager alloc] init]; + sharedInstance.users = [[NSMutableArray alloc] init]; + }); + return sharedInstance; +} + +- (void)addUser:(Person *)user { + if (user) { + [self.users addObject:user]; + NSLog(@"Added user: %@", user.name); + } +} + +- (Person *)findUserByName:(NSString *)name { + for (Person *user in self.users) { + if ([user.name isEqualToString:name]) { + return user; + } + } + return nil; +} + +- (void)removeUser:(Person *)user { + if (user) { + [self.users removeObject:user]; + NSLog(@"Removed user: %@", user.name); + } +} + +- (NSInteger)userCount { + return self.users.count; +} + +@end \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/objective-c/main.m b/reference/code-index-mcp-master/test/sample-projects/objective-c/main.m new file mode 100644 index 00000000..4415d069 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/objective-c/main.m @@ -0,0 +1,30 @@ +#import +#import "Person.h" +#import "UserManager.h" + +int main(int argc, const char * argv[]) { + @autoreleasepool { + // Create some users + Person *alice = [[Person alloc] initWithName:@"Alice" age:25]; + Person *bob = [[Person alloc] initWithName:@"Bob" age:30]; + Person *charlie = [Person createDefaultPerson]; + + // Get shared manager + UserManager *manager = [UserManager sharedManager]; + + // Add users + [manager addUser:alice]; + [manager addUser:bob]; + [manager addUser:charlie]; + + // Find user + Person *found = [manager findUserByName:@"Alice"]; + if (found) { + [found sayHello]; + [found updateEmail:@"alice@example.com"]; + } + + NSLog(@"Total users: %ld", (long)[manager userCount]); + } + return 0; +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/README.md b/reference/code-index-mcp-master/test/sample-projects/python/README.md new file mode 100644 index 00000000..f7ac984f --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/README.md @@ -0,0 +1,144 @@ +# User Management System (Python) + +A comprehensive user management system built in Python for testing Code Index MCP's analysis capabilities. + +## Features + +- **User Management**: Create, update, delete, and search users +- **Authentication**: Password-based authentication with session management +- **Authorization**: Role-based access control (Admin, User, Guest) +- **Data Validation**: Comprehensive input validation and sanitization +- **Export/Import**: JSON and CSV export capabilities +- **CLI Interface**: Command-line interface for system management + +## Project Structure + +``` +user_management/ +├── models/ +│ ├── __init__.py +│ ├── person.py # Basic Person model +│ └── user.py # User model with auth features +├── services/ +│ ├── __init__.py +│ ├── user_manager.py # User management service +│ └── auth_service.py # Authentication service +├── utils/ +│ ├── __init__.py +│ ├── validators.py # Input validation utilities +│ ├── exceptions.py # Custom exception classes +│ └── helpers.py # Helper functions +├── tests/ # Test directory (empty for now) +├── __init__.py +└── cli.py # Command-line interface +``` + +## Installation + +1. Install dependencies: +```bash +pip install -r requirements.txt +``` + +2. Install the package in development mode: +```bash +pip install -e . +``` + +## Usage + +### Running the Demo + +```bash +python main.py +``` + +### Using the CLI + +```bash +# Create a new user +user-cli create-user --name "John Doe" --username "john" --age 30 --email "john@example.com" + +# List all users +user-cli list-users + +# Get user information +user-cli get-user john + +# Update user +user-cli update-user john --age 31 + +# Delete user +user-cli delete-user john + +# Search users +user-cli search "john" + +# Show statistics +user-cli stats + +# Export users +user-cli export --format json --output users.json +``` + +### Programmatic Usage + +```python +from user_management import UserManager, UserRole +from user_management.services.auth_service import AuthService + +# Create user manager +user_manager = UserManager() + +# Create a user +user = user_manager.create_user( + name="Jane Smith", + username="jane", + age=28, + email="jane@example.com", + role=UserRole.USER +) + +# Set password +user.set_password("SecurePass123!") + +# Authenticate +auth_service = AuthService(user_manager) +authenticated_user = auth_service.authenticate("jane", "SecurePass123!") + +# Create session +session_id = auth_service.create_session(authenticated_user) +``` + +## Testing Features + +This project tests the following Python language features: + +- **Classes and Inheritance**: Person and User classes with inheritance +- **Dataclasses**: Modern Python data structures +- **Enums**: Role and status enumerations +- **Type Hints**: Comprehensive type annotations +- **Properties**: Getter/setter methods +- **Class Methods**: Factory methods and utilities +- **Static Methods**: Utility functions +- **Context Managers**: Resource management +- **Decorators**: Method decorators +- **Generators**: Iteration patterns +- **Exception Handling**: Custom exceptions +- **Package Structure**: Modules and imports +- **CLI Development**: Click framework integration +- **JSON/CSV Processing**: Data serialization +- **Regular Expressions**: Input validation +- **Datetime Handling**: Time-based operations +- **Cryptography**: Password hashing +- **File I/O**: Data persistence + +## Dependencies + +- **click**: Command-line interface framework +- **pytest**: Testing framework +- **pydantic**: Data validation (optional) + +## License + +MIT License - This is a sample project for testing purposes. \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/main.py b/reference/code-index-mcp-master/test/sample-projects/python/main.py new file mode 100644 index 00000000..947b9034 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/main.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +Main entry point for the user management system demo. +""" + +from user_management import UserManager, Person, User +from user_management.services.auth_service import AuthService +from user_management.models.user import UserRole +from user_management.utils.exceptions import UserNotFoundError, DuplicateUserError + + +def main(): + """Demonstrate the user management system.""" + print("=" * 50) + print("User Management System Demo") + print("=" * 50) + + # Create user manager and auth service + user_manager = UserManager() + auth_service = AuthService(user_manager) + + # Create some sample users + print("\n1. Creating sample users...") + + try: + # Create admin user + admin = user_manager.create_user( + name="Alice Johnson", + username="alice_admin", + age=30, + email="alice@example.com", + role=UserRole.ADMIN + ) + admin.set_password("AdminPass123!") + admin.add_permission("user_management") + admin.add_permission("system_admin") + + # Create regular users + user1 = user_manager.create_user( + name="Bob Smith", + username="bob_user", + age=25, + email="bob@example.com" + ) + user1.set_password("UserPass123!") + + user2 = user_manager.create_user( + name="Charlie Brown", + username="charlie", + age=35, + email="charlie@example.com" + ) + user2.set_password("CharliePass123!") + + print(f"✓ Created {user_manager.get_user_count()} users") + + except DuplicateUserError as e: + print(f"✗ Error creating users: {e}") + + # Display all users + print("\n2. Listing all users...") + users = user_manager.get_all_users() + for user in users: + print(f" • {user.username} ({user.name}) - {user.role.value}") + + # Test authentication + print("\n3. Testing authentication...") + try: + authenticated_user = auth_service.authenticate("alice_admin", "AdminPass123!") + print(f"✓ Authentication successful for {authenticated_user.username}") + + # Create session + session_id = auth_service.create_session(authenticated_user) + print(f"✓ Session created: {session_id[:16]}...") + + except Exception as e: + print(f"✗ Authentication failed: {e}") + + # Test user search + print("\n4. Testing user search...") + search_results = user_manager.search_users("alice") + print(f"Search results for 'alice': {len(search_results)} users found") + for user in search_results: + print(f" • {user.username} ({user.name})") + + # Test filtering + print("\n5. Testing user filtering...") + older_users = user_manager.get_users_older_than(30) + print(f"Users older than 30: {len(older_users)} users") + for user in older_users: + print(f" • {user.username} ({user.name}) - age {user.age}") + + # Test user updates + print("\n6. Testing user updates...") + try: + updated_user = user_manager.update_user("bob_user", age=26) + print(f"✓ Updated {updated_user.username}'s age to {updated_user.age}") + except UserNotFoundError as e: + print(f"✗ Update failed: {e}") + + # Display statistics + print("\n7. User statistics...") + stats = user_manager.get_user_stats() + for key, value in stats.items(): + print(f" {key.replace('_', ' ').title()}: {value}") + + # Test export functionality + print("\n8. Testing export functionality...") + try: + json_export = user_manager.export_users('json') + print(f"✓ JSON export: {len(json_export)} characters") + + csv_export = user_manager.export_users('csv') + print(f"✓ CSV export: {len(csv_export.splitlines())} lines") + except Exception as e: + print(f"✗ Export failed: {e}") + + # Test password change + print("\n9. Testing password change...") + try: + auth_service.change_password("bob_user", "UserPass123!", "NewUserPass123!") + print("✓ Password changed successfully") + + # Test with new password + auth_service.authenticate("bob_user", "NewUserPass123!") + print("✓ Authentication with new password successful") + + except Exception as e: + print(f"✗ Password change failed: {e}") + + # Test session management + print("\n10. Testing session management...") + session_stats = auth_service.get_session_stats() + print(f"Active sessions: {session_stats['total_sessions']}") + + # Cleanup expired sessions + expired_count = auth_service.cleanup_expired_sessions() + print(f"Cleaned up {expired_count} expired sessions") + + print("\n" + "=" * 50) + print("Demo completed successfully!") + print("=" * 50) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/requirements.txt b/reference/code-index-mcp-master/test/sample-projects/python/requirements.txt new file mode 100644 index 00000000..4d9c502a --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/requirements.txt @@ -0,0 +1,3 @@ +pytest>=7.0.0 +pydantic>=1.10.0 +click>=8.0.0 \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/setup.py b/reference/code-index-mcp-master/test/sample-projects/python/setup.py new file mode 100644 index 00000000..35afb520 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/setup.py @@ -0,0 +1,37 @@ +from setuptools import setup, find_packages + +setup( + name="user-management", + version="0.1.0", + description="A sample user management system for testing Code Index MCP", + author="Test Author", + author_email="test@example.com", + packages=find_packages(), + install_requires=[ + "pydantic>=1.10.0", + "click>=8.0.0", + ], + extras_require={ + "dev": [ + "pytest>=7.0.0", + "black>=22.0.0", + "flake8>=4.0.0", + ] + }, + entry_points={ + "console_scripts": [ + "user-cli=user_management.cli:main", + ], + }, + python_requires=">=3.8", + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + ], +) \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/__init__.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/__init__.py new file mode 100644 index 00000000..e3859435 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/__init__.py @@ -0,0 +1,14 @@ +""" +User Management System + +A sample application for testing Code Index MCP's Python analysis capabilities. +""" + +__version__ = "0.1.0" +__author__ = "Test Author" + +from .models.person import Person +from .models.user import User +from .services.user_manager import UserManager + +__all__ = ["Person", "User", "UserManager"] \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/cli.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/cli.py new file mode 100644 index 00000000..53a0db99 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/cli.py @@ -0,0 +1,246 @@ +""" +Command line interface for user management system. +""" + +import click +import json +from typing import Optional + +from .services.user_manager import UserManager +from .services.auth_service import AuthService +from .models.user import UserRole, UserStatus +from .utils.exceptions import UserNotFoundError, DuplicateUserError + + +@click.group() +@click.pass_context +def cli(ctx): + """User Management System CLI.""" + ctx.ensure_object(dict) + ctx.obj['user_manager'] = UserManager() + ctx.obj['auth_service'] = AuthService(ctx.obj['user_manager']) + + +@cli.command() +@click.option('--name', required=True, help='Full name of the user') +@click.option('--username', required=True, help='Username for the user') +@click.option('--age', required=True, type=int, help='Age of the user') +@click.option('--email', help='Email address of the user') +@click.option('--role', type=click.Choice(['user', 'admin', 'guest']), default='user', help='Role of the user') +@click.option('--password', prompt=True, hide_input=True, help='Password for the user') +@click.pass_context +def create_user(ctx, name: str, username: str, age: int, email: Optional[str], role: str, password: str): + """Create a new user.""" + try: + user_manager = ctx.obj['user_manager'] + user_role = UserRole(role) + + user = user_manager.create_user( + name=name, + username=username, + age=age, + email=email, + role=user_role + ) + + user.set_password(password) + + click.echo(f"User '{username}' created successfully!") + click.echo(f"Name: {user.name}") + click.echo(f"Age: {user.age}") + click.echo(f"Email: {user.email or 'Not provided'}") + click.echo(f"Role: {user.role.value}") + + except DuplicateUserError as e: + click.echo(f"Error: {e}", err=True) + except ValueError as e: + click.echo(f"Error: {e}", err=True) + + +@cli.command() +@click.argument('username') +@click.pass_context +def get_user(ctx, username: str): + """Get information about a user.""" + try: + user_manager = ctx.obj['user_manager'] + user = user_manager.get_user(username) + + click.echo(f"Username: {user.username}") + click.echo(f"Name: {user.name}") + click.echo(f"Age: {user.age}") + click.echo(f"Email: {user.email or 'Not provided'}") + click.echo(f"Role: {user.role.value}") + click.echo(f"Status: {user.status.value}") + click.echo(f"Created: {user.created_at}") + click.echo(f"Last Login: {user.last_login or 'Never'}") + click.echo(f"Permissions: {', '.join(user.permissions) if user.permissions else 'None'}") + + except UserNotFoundError as e: + click.echo(f"Error: {e}", err=True) + + +@cli.command() +@click.pass_context +def list_users(ctx): + """List all users.""" + user_manager = ctx.obj['user_manager'] + users = user_manager.get_all_users() + + if not users: + click.echo("No users found.") + return + + click.echo(f"Found {len(users)} users:") + click.echo("-" * 60) + + for user in users: + status_indicator = "✓" if user.is_active() else "✗" + click.echo(f"{status_indicator} {user.username:<15} {user.name:<20} {user.role.value:<10} {user.email or 'No email'}") + + +@cli.command() +@click.argument('username') +@click.option('--name', help='New name for the user') +@click.option('--age', type=int, help='New age for the user') +@click.option('--email', help='New email for the user') +@click.option('--role', type=click.Choice(['user', 'admin', 'guest']), help='New role for the user') +@click.pass_context +def update_user(ctx, username: str, name: Optional[str], age: Optional[int], + email: Optional[str], role: Optional[str]): + """Update user information.""" + try: + user_manager = ctx.obj['user_manager'] + + updates = {} + if name: + updates['name'] = name + if age is not None: + updates['age'] = age + if email: + updates['email'] = email + if role: + updates['role'] = UserRole(role) + + if not updates: + click.echo("No updates provided.") + return + + user = user_manager.update_user(username, **updates) + click.echo(f"User '{username}' updated successfully!") + + except UserNotFoundError as e: + click.echo(f"Error: {e}", err=True) + except ValueError as e: + click.echo(f"Error: {e}", err=True) + + +@cli.command() +@click.argument('username') +@click.confirmation_option(prompt='Are you sure you want to delete this user?') +@click.pass_context +def delete_user(ctx, username: str): + """Delete a user.""" + try: + user_manager = ctx.obj['user_manager'] + user_manager.delete_user(username) + click.echo(f"User '{username}' deleted successfully!") + + except UserNotFoundError as e: + click.echo(f"Error: {e}", err=True) + + +@cli.command() +@click.argument('username') +@click.option('--password', prompt=True, hide_input=True, help='Password for authentication') +@click.pass_context +def authenticate(ctx, username: str, password: str): + """Authenticate a user.""" + try: + auth_service = ctx.obj['auth_service'] + user = auth_service.authenticate(username, password) + + click.echo(f"Authentication successful!") + click.echo(f"Welcome, {user.name}!") + + # Create a session + session_id = auth_service.create_session(user) + click.echo(f"Session created: {session_id}") + + except Exception as e: + click.echo(f"Authentication failed: {e}", err=True) + + +@cli.command() +@click.pass_context +def stats(ctx): + """Show user statistics.""" + user_manager = ctx.obj['user_manager'] + auth_service = ctx.obj['auth_service'] + + user_stats = user_manager.get_user_stats() + session_stats = auth_service.get_session_stats() + + click.echo("User Statistics:") + click.echo(f" Total Users: {user_stats['total']}") + click.echo(f" Active Users: {user_stats['active']}") + click.echo(f" Admin Users: {user_stats['admin']}") + click.echo(f" Regular Users: {user_stats['user']}") + click.echo(f" Guest Users: {user_stats['guest']}") + click.echo(f" Users with Email: {user_stats['with_email']}") + + click.echo("\nSession Statistics:") + click.echo(f" Active Sessions: {session_stats['total_sessions']}") + click.echo(f" Recent Sessions: {session_stats['recent_sessions']}") + click.echo(f" Old Sessions: {session_stats['old_sessions']}") + + +@cli.command() +@click.option('--format', type=click.Choice(['json', 'csv']), default='json', help='Export format') +@click.option('--output', help='Output file path') +@click.pass_context +def export(ctx, format: str, output: Optional[str]): + """Export users to file.""" + user_manager = ctx.obj['user_manager'] + + try: + data = user_manager.export_users(format) + + if output: + with open(output, 'w') as f: + f.write(data) + click.echo(f"Users exported to {output}") + else: + click.echo(data) + + except Exception as e: + click.echo(f"Export failed: {e}", err=True) + + +@cli.command() +@click.argument('query') +@click.pass_context +def search(ctx, query: str): + """Search users by name or username.""" + user_manager = ctx.obj['user_manager'] + users = user_manager.search_users(query) + + if not users: + click.echo(f"No users found matching '{query}'") + return + + click.echo(f"Found {len(users)} users matching '{query}':") + click.echo("-" * 60) + + for user in users: + status_indicator = "✓" if user.is_active() else "✗" + click.echo(f"{status_indicator} {user.username:<15} {user.name:<20} {user.role.value:<10}") + + +def main(): + """Main entry point for the CLI.""" + cli() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/__init__.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/__init__.py new file mode 100644 index 00000000..3b63ae74 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/__init__.py @@ -0,0 +1,6 @@ +"""Models package for user management system.""" + +from .person import Person +from .user import User + +__all__ = ["Person", "User"] \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/person.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/person.py new file mode 100644 index 00000000..eba75dac --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/person.py @@ -0,0 +1,91 @@ +""" +Person model for the user management system. +""" + +from typing import Optional, Dict, Any +from dataclasses import dataclass, field +from datetime import datetime +import json + + +@dataclass +class Person: + """Represents a person with basic information.""" + + name: str + age: int + email: Optional[str] = None + created_at: datetime = field(default_factory=datetime.now) + metadata: Dict[str, Any] = field(default_factory=dict) + + def __post_init__(self): + """Validate data after initialization.""" + if self.age < 0: + raise ValueError("Age cannot be negative") + if self.age > 150: + raise ValueError("Age cannot be greater than 150") + if not self.name.strip(): + raise ValueError("Name cannot be empty") + + def greet(self) -> str: + """Return a greeting message.""" + return f"Hello, I'm {self.name} and I'm {self.age} years old." + + def has_email(self) -> bool: + """Check if person has an email address.""" + return self.email is not None and self.email.strip() != "" + + def update_email(self, email: str) -> None: + """Update the person's email address.""" + if not email.strip(): + raise ValueError("Email cannot be empty") + self.email = email.strip() + + def add_metadata(self, key: str, value: Any) -> None: + """Add metadata to the person.""" + self.metadata[key] = value + + def get_metadata(self, key: str, default: Any = None) -> Any: + """Get metadata value by key.""" + return self.metadata.get(key, default) + + def to_dict(self) -> Dict[str, Any]: + """Convert person to dictionary.""" + return { + "name": self.name, + "age": self.age, + "email": self.email, + "created_at": self.created_at.isoformat(), + "metadata": self.metadata + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Person': + """Create a Person from a dictionary.""" + created_at = datetime.fromisoformat(data.get("created_at", datetime.now().isoformat())) + return cls( + name=data["name"], + age=data["age"], + email=data.get("email"), + created_at=created_at, + metadata=data.get("metadata", {}) + ) + + def to_json(self) -> str: + """Convert person to JSON string.""" + return json.dumps(self.to_dict(), indent=2) + + @classmethod + def from_json(cls, json_str: str) -> 'Person': + """Create a Person from JSON string.""" + data = json.loads(json_str) + return cls.from_dict(data) + + def __str__(self) -> str: + """String representation of person.""" + email_str = f", email: {self.email}" if self.has_email() else "" + return f"Person(name: {self.name}, age: {self.age}{email_str})" + + def __repr__(self) -> str: + """Developer representation of person.""" + return f"Person(name='{self.name}', age={self.age}, email='{self.email}')" \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/user.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/user.py new file mode 100644 index 00000000..83e1f13e --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/models/user.py @@ -0,0 +1,180 @@ +""" +User model extending Person for the user management system. +""" + +from typing import Optional, Dict, Any, Set +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +import hashlib +import secrets + +from .person import Person + + +class UserRole(Enum): + """User role enumeration.""" + ADMIN = "admin" + USER = "user" + GUEST = "guest" + + +class UserStatus(Enum): + """User status enumeration.""" + ACTIVE = "active" + INACTIVE = "inactive" + SUSPENDED = "suspended" + DELETED = "deleted" + + +@dataclass +class User(Person): + """User class extending Person with authentication and permissions.""" + + username: str = "" + password_hash: str = "" + role: UserRole = UserRole.USER + status: UserStatus = UserStatus.ACTIVE + last_login: Optional[datetime] = None + login_attempts: int = 0 + permissions: Set[str] = field(default_factory=set) + + def __post_init__(self): + """Validate user data after initialization.""" + super().__post_init__() + if not self.username.strip(): + raise ValueError("Username cannot be empty") + if len(self.username) < 3: + raise ValueError("Username must be at least 3 characters long") + + def set_password(self, password: str) -> None: + """Set user password with hashing.""" + if len(password) < 8: + raise ValueError("Password must be at least 8 characters long") + + # Simple password hashing (in real app, use bcrypt) + salt = secrets.token_hex(16) + password_hash = hashlib.pbkdf2_hmac('sha256', + password.encode('utf-8'), + salt.encode('utf-8'), + 100000) + self.password_hash = salt + password_hash.hex() + + def verify_password(self, password: str) -> bool: + """Verify password against stored hash.""" + if not self.password_hash: + return False + + try: + salt = self.password_hash[:32] + stored_hash = self.password_hash[32:] + + password_hash = hashlib.pbkdf2_hmac('sha256', + password.encode('utf-8'), + salt.encode('utf-8'), + 100000) + + return password_hash.hex() == stored_hash + except Exception: + return False + + def add_permission(self, permission: str) -> None: + """Add a permission to the user.""" + self.permissions.add(permission) + + def remove_permission(self, permission: str) -> None: + """Remove a permission from the user.""" + self.permissions.discard(permission) + + def has_permission(self, permission: str) -> bool: + """Check if user has a specific permission.""" + return permission in self.permissions + + def is_admin(self) -> bool: + """Check if user is an admin.""" + return self.role == UserRole.ADMIN + + def is_active(self) -> bool: + """Check if user is active.""" + return self.status == UserStatus.ACTIVE + + def login(self) -> bool: + """Record a successful login.""" + if not self.is_active(): + return False + + self.last_login = datetime.now() + self.login_attempts = 0 + return True + + def failed_login_attempt(self) -> None: + """Record a failed login attempt.""" + self.login_attempts += 1 + if self.login_attempts >= 5: + self.status = UserStatus.SUSPENDED + + def activate(self) -> None: + """Activate the user account.""" + self.status = UserStatus.ACTIVE + self.login_attempts = 0 + + def deactivate(self) -> None: + """Deactivate the user account.""" + self.status = UserStatus.INACTIVE + + def suspend(self) -> None: + """Suspend the user account.""" + self.status = UserStatus.SUSPENDED + + def delete(self) -> None: + """Mark the user as deleted.""" + self.status = UserStatus.DELETED + + def to_dict(self) -> Dict[str, Any]: + """Convert user to dictionary.""" + data = super().to_dict() + data.update({ + "username": self.username, + "role": self.role.value, + "status": self.status.value, + "last_login": self.last_login.isoformat() if self.last_login else None, + "login_attempts": self.login_attempts, + "permissions": list(self.permissions), + }) + return data + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'User': + """Create a User from a dictionary.""" + person_data = {k: v for k, v in data.items() + if k in ["name", "age", "email", "created_at", "metadata"]} + person = Person.from_dict(person_data) + + last_login = None + if data.get("last_login"): + last_login = datetime.fromisoformat(data["last_login"]) + + return cls( + name=person.name, + age=person.age, + email=person.email, + created_at=person.created_at, + metadata=person.metadata, + username=data["username"], + password_hash=data.get("password_hash", ""), + role=UserRole(data.get("role", UserRole.USER.value)), + status=UserStatus(data.get("status", UserStatus.ACTIVE.value)), + last_login=last_login, + login_attempts=data.get("login_attempts", 0), + permissions=set(data.get("permissions", [])) + ) + + def __str__(self) -> str: + """String representation of user.""" + return f"User(username: {self.username}, name: {self.name}, role: {self.role.value})" + + def __repr__(self) -> str: + """Developer representation of user.""" + return f"User(username='{self.username}', name='{self.name}', role={self.role})" + +# AUTO_REINDEX_MARKER: ci_auto_reindex_test_token \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/__init__.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/__init__.py new file mode 100644 index 00000000..6ebefbf4 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/__init__.py @@ -0,0 +1,6 @@ +"""Services package for user management system.""" + +from .user_manager import UserManager +from .auth_service import AuthService + +__all__ = ["UserManager", "AuthService"] \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/auth_service.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/auth_service.py new file mode 100644 index 00000000..21d8d59d --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/auth_service.py @@ -0,0 +1,185 @@ +""" +Authentication service for user management system. +""" + +from typing import Optional, Dict, Any +from datetime import datetime, timedelta +import secrets +import hashlib + +from ..models.user import User, UserStatus +from ..utils.exceptions import AuthenticationError, UserNotFoundError + + +class AuthService: + """Service class for handling authentication operations.""" + + def __init__(self, user_manager): + """Initialize the authentication service with a user manager.""" + self._user_manager = user_manager + self._active_sessions: Dict[str, Dict[str, Any]] = {} + self._session_timeout = timedelta(hours=24) + + def authenticate(self, username: str, password: str) -> Optional[User]: + """Authenticate a user with username and password.""" + try: + user = self._user_manager.get_user(username) + except UserNotFoundError: + raise AuthenticationError("Invalid username or password") + + if not user.is_active(): + raise AuthenticationError("User account is not active") + + if not user.verify_password(password): + user.failed_login_attempt() + raise AuthenticationError("Invalid username or password") + + # Successful authentication + user.login() + return user + + def create_session(self, user: User) -> str: + """Create a new session for the authenticated user.""" + session_id = secrets.token_urlsafe(32) + session_data = { + 'user_id': user.username, + 'created_at': datetime.now(), + 'last_activity': datetime.now(), + 'ip_address': None, # Would be set in a real application + 'user_agent': None, # Would be set in a real application + } + + self._active_sessions[session_id] = session_data + return session_id + + def validate_session(self, session_id: str) -> Optional[User]: + """Validate a session and return the associated user.""" + if session_id not in self._active_sessions: + return None + + session_data = self._active_sessions[session_id] + + # Check if session has expired + if datetime.now() - session_data['last_activity'] > self._session_timeout: + self.destroy_session(session_id) + return None + + # Update last activity + session_data['last_activity'] = datetime.now() + + try: + user = self._user_manager.get_user(session_data['user_id']) + if not user.is_active(): + self.destroy_session(session_id) + return None + return user + except UserNotFoundError: + self.destroy_session(session_id) + return None + + def destroy_session(self, session_id: str) -> bool: + """Destroy a session.""" + if session_id in self._active_sessions: + del self._active_sessions[session_id] + return True + return False + + def destroy_all_sessions(self, username: str) -> int: + """Destroy all sessions for a specific user.""" + sessions_to_remove = [] + for session_id, session_data in self._active_sessions.items(): + if session_data['user_id'] == username: + sessions_to_remove.append(session_id) + + for session_id in sessions_to_remove: + del self._active_sessions[session_id] + + return len(sessions_to_remove) + + def get_active_sessions(self, username: str) -> List[Dict[str, Any]]: + """Get all active sessions for a user.""" + sessions = [] + for session_id, session_data in self._active_sessions.items(): + if session_data['user_id'] == username: + sessions.append({ + 'session_id': session_id, + 'created_at': session_data['created_at'], + 'last_activity': session_data['last_activity'], + 'ip_address': session_data.get('ip_address'), + 'user_agent': session_data.get('user_agent'), + }) + return sessions + + def cleanup_expired_sessions(self) -> int: + """Remove expired sessions and return count of removed sessions.""" + current_time = datetime.now() + expired_sessions = [] + + for session_id, session_data in self._active_sessions.items(): + if current_time - session_data['last_activity'] > self._session_timeout: + expired_sessions.append(session_id) + + for session_id in expired_sessions: + del self._active_sessions[session_id] + + return len(expired_sessions) + + def change_password(self, username: str, old_password: str, new_password: str) -> bool: + """Change a user's password.""" + user = self._user_manager.get_user(username) + + if not user.verify_password(old_password): + raise AuthenticationError("Current password is incorrect") + + user.set_password(new_password) + + # Destroy all existing sessions for security + self.destroy_all_sessions(username) + + return True + + def reset_password(self, username: str, new_password: str) -> str: + """Reset a user's password (admin function).""" + user = self._user_manager.get_user(username) + + # Generate a temporary password if none provided + if not new_password: + new_password = self._generate_temporary_password() + + user.set_password(new_password) + + # Destroy all existing sessions + self.destroy_all_sessions(username) + + return new_password + + def _generate_temporary_password(self) -> str: + """Generate a temporary password.""" + return secrets.token_urlsafe(12) + + def get_session_stats(self) -> Dict[str, Any]: + """Get statistics about active sessions.""" + current_time = datetime.now() + session_count = len(self._active_sessions) + + # Count sessions by age + recent_sessions = 0 # Last hour + old_sessions = 0 # Older than 1 hour + + for session_data in self._active_sessions.values(): + age = current_time - session_data['last_activity'] + if age < timedelta(hours=1): + recent_sessions += 1 + else: + old_sessions += 1 + + return { + 'total_sessions': session_count, + 'recent_sessions': recent_sessions, + 'old_sessions': old_sessions, + 'session_timeout_hours': self._session_timeout.total_seconds() / 3600, + } + + def __str__(self) -> str: + """String representation of AuthService.""" + return f"AuthService(active_sessions: {len(self._active_sessions)})" \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/user_manager.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/user_manager.py new file mode 100644 index 00000000..05ca4bce --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/services/user_manager.py @@ -0,0 +1,222 @@ +""" +User management service for handling user operations. +""" + +from typing import List, Optional, Dict, Any, Callable +from datetime import datetime +import json +import os + +from ..models.user import User, UserRole, UserStatus +from ..utils.validators import validate_email, validate_username +from ..utils.exceptions import UserNotFoundError, DuplicateUserError + + +class UserManager: + """Service class for managing users.""" + + def __init__(self, storage_path: Optional[str] = None): + """Initialize the user manager with optional storage path.""" + self._users: Dict[str, User] = {} + self._storage_path = storage_path + if storage_path and os.path.exists(storage_path): + self._load_from_file() + + def create_user(self, name: str, age: int, username: str, + email: Optional[str] = None, + role: UserRole = UserRole.USER) -> User: + """Create a new user.""" + if username in self._users: + raise DuplicateUserError(f"User with username '{username}' already exists") + + # Validate inputs + if not validate_username(username): + raise ValueError("Invalid username format") + + if email and not validate_email(email): + raise ValueError("Invalid email format") + + user = User( + name=name, + age=age, + username=username, + email=email, + role=role + ) + + self._users[username] = user + self._save_to_file() + return user + + def get_user(self, username: str) -> User: + """Get a user by username.""" + if username not in self._users: + raise UserNotFoundError(f"User with username '{username}' not found") + return self._users[username] + + def get_user_by_email(self, email: str) -> Optional[User]: + """Get a user by email address.""" + for user in self._users.values(): + if user.email == email: + return user + return None + + def update_user(self, username: str, **kwargs) -> User: + """Update user information.""" + user = self.get_user(username) + + # Update allowed fields + allowed_fields = ['name', 'age', 'email', 'role', 'status'] + for field, value in kwargs.items(): + if field in allowed_fields and hasattr(user, field): + setattr(user, field, value) + + self._save_to_file() + return user + + def delete_user(self, username: str) -> bool: + """Delete a user (soft delete).""" + user = self.get_user(username) + user.delete() + self._save_to_file() + return True + + def remove_user(self, username: str) -> bool: + """Remove a user completely from the system.""" + if username not in self._users: + raise UserNotFoundError(f"User with username '{username}' not found") + + del self._users[username] + self._save_to_file() + return True + + def get_all_users(self) -> List[User]: + """Get all users.""" + return list(self._users.values()) + + def get_active_users(self) -> List[User]: + """Get all active users.""" + return [user for user in self._users.values() if user.is_active()] + + def get_users_by_role(self, role: UserRole) -> List[User]: + """Get users by role.""" + return [user for user in self._users.values() if user.role == role] + + def filter_users(self, filter_func: Callable[[User], bool]) -> List[User]: + """Filter users using a custom function.""" + return [user for user in self._users.values() if filter_func(user)] + + def search_users(self, query: str) -> List[User]: + """Search users by name or username.""" + query_lower = query.lower() + return [ + user for user in self._users.values() + if query_lower in user.name.lower() or query_lower in user.username.lower() + ] + + def get_users_older_than(self, age: int) -> List[User]: + """Get users older than specified age.""" + return self.filter_users(lambda user: user.age > age) + + def get_users_with_email(self) -> List[User]: + """Get users that have email addresses.""" + return self.filter_users(lambda user: user.has_email()) + + def get_users_with_permission(self, permission: str) -> List[User]: + """Get users with specific permission.""" + return self.filter_users(lambda user: user.has_permission(permission)) + + def get_user_count(self) -> int: + """Get the total number of users.""" + return len(self._users) + + def get_user_stats(self) -> Dict[str, int]: + """Get user statistics.""" + stats = { + 'total': len(self._users), + 'active': len(self.get_active_users()), + 'admin': len(self.get_users_by_role(UserRole.ADMIN)), + 'user': len(self.get_users_by_role(UserRole.USER)), + 'guest': len(self.get_users_by_role(UserRole.GUEST)), + 'with_email': len(self.get_users_with_email()), + } + return stats + + def export_users(self, format: str = 'json') -> str: + """Export users to specified format.""" + if format.lower() == 'json': + return self._export_to_json() + elif format.lower() == 'csv': + return self._export_to_csv() + else: + raise ValueError(f"Unsupported export format: {format}") + + def _export_to_json(self) -> str: + """Export users to JSON format.""" + users_data = [user.to_dict() for user in self._users.values()] + return json.dumps(users_data, indent=2) + + def _export_to_csv(self) -> str: + """Export users to CSV format.""" + if not self._users: + return "username,name,age,email,role,status\n" + + lines = ["username,name,age,email,role,status"] + for user in self._users.values(): + line = f"{user.username},{user.name},{user.age},{user.email or ''},{user.role.value},{user.status.value}" + lines.append(line) + + return "\n".join(lines) + + def _save_to_file(self) -> None: + """Save users to file if storage path is set.""" + if not self._storage_path: + return + + try: + with open(self._storage_path, 'w') as f: + json.dump(self._export_to_json(), f, indent=2) + except Exception as e: + print(f"Error saving users to file: {e}") + + def _load_from_file(self) -> None: + """Load users from file.""" + if not self._storage_path or not os.path.exists(self._storage_path): + return + + try: + with open(self._storage_path, 'r') as f: + data = json.load(f) + if isinstance(data, str): + data = json.loads(data) + + for user_data in data: + user = User.from_dict(user_data) + self._users[user.username] = user + except Exception as e: + print(f"Error loading users from file: {e}") + + def clear_all_users(self) -> None: + """Clear all users from the system.""" + self._users.clear() + self._save_to_file() + + def __len__(self) -> int: + """Return the number of users.""" + return len(self._users) + + def __contains__(self, username: str) -> bool: + """Check if a username exists.""" + return username in self._users + + def __iter__(self): + """Iterate over users.""" + return iter(self._users.values()) + + def __str__(self) -> str: + """String representation of UserManager.""" + return f"UserManager(users: {len(self._users)})" + + # CI marker method to verify auto-reindex on change + def _ci_added_symbol_marker(self) -> str: + return "ci_symbol_python" \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/__init__.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/__init__.py new file mode 100644 index 00000000..82b229c9 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/__init__.py @@ -0,0 +1,17 @@ +"""Utilities package for user management system.""" + +from .validators import validate_email, validate_username, validate_password +from .exceptions import UserNotFoundError, DuplicateUserError, AuthenticationError +from .helpers import generate_random_string, format_datetime, parse_datetime + +__all__ = [ + "validate_email", + "validate_username", + "validate_password", + "UserNotFoundError", + "DuplicateUserError", + "AuthenticationError", + "generate_random_string", + "format_datetime", + "parse_datetime" +] \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/exceptions.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/exceptions.py new file mode 100644 index 00000000..cc844794 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/exceptions.py @@ -0,0 +1,80 @@ +""" +Custom exceptions for user management system. +""" + + +class UserManagementError(Exception): + """Base exception for user management errors.""" + pass + + +class UserNotFoundError(UserManagementError): + """Exception raised when a user is not found.""" + + def __init__(self, message: str = "User not found"): + self.message = message + super().__init__(self.message) + + +class DuplicateUserError(UserManagementError): + """Exception raised when trying to create a user that already exists.""" + + def __init__(self, message: str = "User already exists"): + self.message = message + super().__init__(self.message) + + +class AuthenticationError(UserManagementError): + """Exception raised when authentication fails.""" + + def __init__(self, message: str = "Authentication failed"): + self.message = message + super().__init__(self.message) + + +class AuthorizationError(UserManagementError): + """Exception raised when authorization fails.""" + + def __init__(self, message: str = "Authorization failed"): + self.message = message + super().__init__(self.message) + + +class ValidationError(UserManagementError): + """Exception raised when validation fails.""" + + def __init__(self, message: str = "Validation failed"): + self.message = message + super().__init__(self.message) + + +class PermissionError(UserManagementError): + """Exception raised when user lacks required permissions.""" + + def __init__(self, message: str = "Permission denied"): + self.message = message + super().__init__(self.message) + + +class SessionError(UserManagementError): + """Exception raised when session operations fail.""" + + def __init__(self, message: str = "Session error"): + self.message = message + super().__init__(self.message) + + +class StorageError(UserManagementError): + """Exception raised when storage operations fail.""" + + def __init__(self, message: str = "Storage error"): + self.message = message + super().__init__(self.message) + + +class ConfigurationError(UserManagementError): + """Exception raised when configuration is invalid.""" + + def __init__(self, message: str = "Configuration error"): + self.message = message + super().__init__(self.message) \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/helpers.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/helpers.py new file mode 100644 index 00000000..0c18d065 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/helpers.py @@ -0,0 +1,206 @@ +""" +Helper utilities for user management system. +""" + +import secrets +import string +from datetime import datetime, timezone +from typing import Optional, Dict, Any, List +import json +import hashlib + + +def generate_random_string(length: int = 16, + include_digits: bool = True, + include_symbols: bool = False) -> str: + """Generate a random string of specified length.""" + characters = string.ascii_letters + + if include_digits: + characters += string.digits + + if include_symbols: + characters += "!@#$%^&*" + + return ''.join(secrets.choice(characters) for _ in range(length)) + + +def generate_secure_token(length: int = 32) -> str: + """Generate a secure URL-safe token.""" + return secrets.token_urlsafe(length) + + +def generate_hash(input_string: str, salt: str = "") -> str: + """Generate a SHA-256 hash of the input string.""" + combined = f"{input_string}{salt}" + return hashlib.sha256(combined.encode()).hexdigest() + + +def format_datetime(dt: datetime, format_str: str = "%Y-%m-%d %H:%M:%S") -> str: + """Format datetime object to string.""" + if dt is None: + return "" + return dt.strftime(format_str) + + +def parse_datetime(date_string: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> Optional[datetime]: + """Parse string to datetime object.""" + try: + return datetime.strptime(date_string, format_str) + except ValueError: + return None + + +def get_current_timestamp() -> str: + """Get current timestamp as ISO format string.""" + return datetime.now(timezone.utc).isoformat() + + +def is_valid_json(json_string: str) -> bool: + """Check if a string is valid JSON.""" + try: + json.loads(json_string) + return True + except (ValueError, TypeError): + return False + + +def deep_merge_dicts(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]: + """Deep merge two dictionaries.""" + result = dict1.copy() + + for key, value in dict2.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = deep_merge_dicts(result[key], value) + else: + result[key] = value + + return result + + +def flatten_dict(nested_dict: Dict[str, Any], parent_key: str = '', sep: str = '.') -> Dict[str, Any]: + """Flatten a nested dictionary.""" + items = [] + + for key, value in nested_dict.items(): + new_key = f"{parent_key}{sep}{key}" if parent_key else key + + if isinstance(value, dict): + items.extend(flatten_dict(value, new_key, sep=sep).items()) + else: + items.append((new_key, value)) + + return dict(items) + + +def chunk_list(input_list: List[Any], chunk_size: int) -> List[List[Any]]: + """Split a list into chunks of specified size.""" + return [input_list[i:i + chunk_size] for i in range(0, len(input_list), chunk_size)] + + +def remove_duplicates(input_list: List[Any]) -> List[Any]: + """Remove duplicates from a list while preserving order.""" + seen = set() + result = [] + + for item in input_list: + if item not in seen: + seen.add(item) + result.append(item) + + return result + + +def safe_dict_get(dictionary: Dict[str, Any], key_path: str, default: Any = None) -> Any: + """Safely get value from nested dictionary using dot notation.""" + keys = key_path.split('.') + current = dictionary + + try: + for key in keys: + current = current[key] + return current + except (KeyError, TypeError): + return default + + +def calculate_age(birth_date: datetime) -> int: + """Calculate age from birth date.""" + today = datetime.now() + age = today.year - birth_date.year + + # Adjust if birthday hasn't occurred this year + if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day): + age -= 1 + + return age + + +def mask_email(email: str) -> str: + """Mask email address for privacy.""" + if not email or '@' not in email: + return email + + username, domain = email.split('@', 1) + + if len(username) <= 2: + masked_username = username + else: + masked_username = username[0] + '*' * (len(username) - 2) + username[-1] + + return f"{masked_username}@{domain}" + + +def format_file_size(size_bytes: int) -> str: + """Format file size in human readable format.""" + if size_bytes == 0: + return "0 B" + + size_names = ["B", "KB", "MB", "GB", "TB"] + i = 0 + + while size_bytes >= 1024 and i < len(size_names) - 1: + size_bytes /= 1024.0 + i += 1 + + return f"{size_bytes:.1f} {size_names[i]}" + + +def truncate_string(text: str, max_length: int, suffix: str = "...") -> str: + """Truncate string to specified length with optional suffix.""" + if len(text) <= max_length: + return text + + return text[:max_length - len(suffix)] + suffix + + +def camel_to_snake(name: str) -> str: + """Convert camelCase to snake_case.""" + import re + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +def snake_to_camel(name: str) -> str: + """Convert snake_case to camelCase.""" + components = name.split('_') + return components[0] + ''.join(x.title() for x in components[1:]) + + +def is_email_domain_valid(email: str, allowed_domains: List[str]) -> bool: + """Check if email domain is in allowed list.""" + if not email or '@' not in email: + return False + + domain = email.split('@')[1].lower() + return domain in [d.lower() for d in allowed_domains] + + +def get_initials(name: str) -> str: + """Get initials from a name.""" + if not name: + return "" + + words = name.strip().split() + initials = ''.join(word[0].upper() for word in words if word) + return initials[:3] # Limit to 3 characters \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/validators.py b/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/validators.py new file mode 100644 index 00000000..55d8dc36 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/python/user_management/utils/validators.py @@ -0,0 +1,132 @@ +""" +Validation utilities for user management system. +""" + +import re +from typing import Optional + + +def validate_email(email: str) -> bool: + """Validate email address format.""" + if not email: + return False + + # Basic email validation pattern + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return bool(re.match(pattern, email)) + + +def validate_username(username: str) -> bool: + """Validate username format.""" + if not username: + return False + + # Username must be 3-20 characters, alphanumeric and underscores only + if len(username) < 3 or len(username) > 20: + return False + + pattern = r'^[a-zA-Z0-9_]+$' + return bool(re.match(pattern, username)) + + +def validate_password(password: str) -> tuple[bool, Optional[str]]: + """ + Validate password strength. + + Returns: + tuple: (is_valid, error_message) + """ + if not password: + return False, "Password cannot be empty" + + if len(password) < 8: + return False, "Password must be at least 8 characters long" + + if len(password) > 128: + return False, "Password must be no more than 128 characters long" + + # Check for at least one uppercase letter + if not re.search(r'[A-Z]', password): + return False, "Password must contain at least one uppercase letter" + + # Check for at least one lowercase letter + if not re.search(r'[a-z]', password): + return False, "Password must contain at least one lowercase letter" + + # Check for at least one digit + if not re.search(r'\d', password): + return False, "Password must contain at least one digit" + + # Check for at least one special character + if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password): + return False, "Password must contain at least one special character" + + return True, None + + +def validate_age(age: int) -> bool: + """Validate age value.""" + return 0 <= age <= 150 + + +def validate_name(name: str) -> bool: + """Validate name format.""" + if not name or not name.strip(): + return False + + # Name should be 1-50 characters, letters, spaces, hyphens, and apostrophes + if len(name.strip()) > 50: + return False + + pattern = r"^[a-zA-Z\s\-']+$" + return bool(re.match(pattern, name.strip())) + + +def validate_phone(phone: str) -> bool: + """Validate phone number format.""" + if not phone: + return False + + # Remove all non-digit characters + digits_only = re.sub(r'\D', '', phone) + + # Check if it's a valid length (10-15 digits) + return 10 <= len(digits_only) <= 15 + + +def sanitize_input(input_str: str) -> str: + """Sanitize user input by removing potentially dangerous characters.""" + if not input_str: + return "" + + # Remove HTML tags + clean_str = re.sub(r'<[^>]+>', '', input_str) + + # Remove script tags and their content + clean_str = re.sub(r'', '', clean_str, flags=re.DOTALL | re.IGNORECASE) + + # Remove dangerous characters + dangerous_chars = ['<', '>', '"', "'", '&', ';', '`', '|', '$', '(', ')', '{', '}', '[', ']'] + for char in dangerous_chars: + clean_str = clean_str.replace(char, '') + + return clean_str.strip() + + +def validate_url(url: str) -> bool: + """Validate URL format.""" + if not url: + return False + + pattern = r'^https?://(?:[-\w.])+(?::[0-9]+)?(?:/[^?\s]*)?(?:\?[^#\s]*)?(?:#[^\s]*)?$' + return bool(re.match(pattern, url)) + + +def validate_json_string(json_str: str) -> bool: + """Validate if a string is valid JSON.""" + try: + import json + json.loads(json_str) + return True + except (ValueError, TypeError): + return False \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/sample.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/sample.ts new file mode 100644 index 00000000..d5c3a6d2 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/sample.ts @@ -0,0 +1,187 @@ +/** + * Sample TypeScript file for testing Code Index MCP analysis. + */ + +interface PersonInterface { + name: string; + age: number; + email?: string; +} + +interface UserManagerInterface { + addUser(person: Person): void; + findByName(name: string): Person | null; + getAllUsers(): Person[]; + getUserCount(): number; +} + +/** + * Represents a person with basic information. + */ +class Person implements PersonInterface { + public readonly name: string; + public readonly age: number; + public email?: string; + + constructor(name: string, age: number, email?: string) { + if (age < 0) { + throw new Error("Age cannot be negative"); + } + + this.name = name; + this.age = age; + this.email = email; + } + + /** + * Returns a greeting message. + */ + greet(): string { + return `Hello, I'm ${this.name} and I'm ${this.age} years old.`; + } + + /** + * Update the person's email. + */ + updateEmail(email: string): void { + this.email = email; + } + + /** + * Create a Person from an object. + */ + static fromObject(data: PersonInterface): Person { + return new Person(data.name, data.age, data.email); + } + + /** + * Convert person to JSON-serializable object. + */ + toJSON(): PersonInterface { + return { + name: this.name, + age: this.age, + email: this.email + }; + } +} + +/** + * Generic utility type for filtering arrays. + */ +type FilterFunction = (item: T) => boolean; + +/** + * Manages a collection of users. + */ +class UserManager implements UserManagerInterface { + private users: Person[] = []; + + /** + * Add a user to the collection. + */ + addUser(person: Person): void { + this.users.push(person); + } + + /** + * Find a user by name. + */ + findByName(name: string): Person | null { + const user = this.users.find(user => user.name === name); + return user || null; + } + + /** + * Get all users. + */ + getAllUsers(): Person[] { + return [...this.users]; + } + + /** + * Filter users by a custom function. + */ + filterUsers(filterFn: FilterFunction): Person[] { + return this.users.filter(filterFn); + } + + /** + * Get users older than specified age. + */ + getUsersOlderThan(age: number): Person[] { + return this.filterUsers(user => user.age > age); + } + + /** + * Get users with email addresses. + */ + getUsersWithEmail(): Person[] { + return this.filterUsers(user => !!user.email); + } + + /** + * Get the number of users. + */ + getUserCount(): number { + return this.users.length; + } + + /** + * Export all users as JSON. + */ + exportToJSON(): string { + return JSON.stringify(this.users.map(user => user.toJSON()), null, 2); + } +} + +/** + * Utility functions for working with users. + */ +namespace UserUtils { + export function validateAge(age: number): boolean { + return age >= 0 && age <= 150; + } + + export function formatUserList(users: Person[]): string { + return users.map(user => user.greet()).join('\n'); + } + + export const DEFAULT_USERS: PersonInterface[] = [ + { name: "Alice", age: 30, email: "alice@example.com" }, + { name: "Bob", age: 25 }, + { name: "Charlie", age: 35, email: "charlie@example.com" } + ]; +} + +/** + * Main function to demonstrate usage. + */ +function main(): void { + const manager = new UserManager(); + + // Add some users + UserUtils.DEFAULT_USERS.forEach(userData => { + const person = Person.fromObject(userData); + manager.addUser(person); + console.log(person.greet()); + }); + + console.log(`Total users: ${manager.getUserCount()}`); + + // Find users older than 25 + const olderUsers = manager.getUsersOlderThan(25); + console.log(`Users older than 25: ${olderUsers.length}`); + + // Export to JSON + console.log("Users as JSON:"); + console.log(manager.exportToJSON()); +} + +// Export for module usage +export { Person, UserManager, UserUtils, PersonInterface, UserManagerInterface }; + +// Run main if this is the entry point +if (require.main === module) { + main(); +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/package.json b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/package.json new file mode 100644 index 00000000..5a7454f8 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/package.json @@ -0,0 +1,121 @@ +{ + "name": "user-management-ts", + "version": "1.0.0", + "description": "A comprehensive TypeScript user management system for testing Code Index MCP", + "main": "dist/server.js", + "scripts": { + "build": "tsc", + "start": "node dist/server.js", + "dev": "ts-node-dev --respawn --transpile-only src/server.ts", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src/ --ext .ts --fix", + "format": "prettier --write src/", + "clean": "rimraf dist", + "prebuild": "npm run clean", + "prestart": "npm run build" + }, + "keywords": [ + "user-management", + "typescript", + "nodejs", + "express", + "authentication", + "api" + ], + "author": "Test Author", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mongoose": "^7.4.1", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.1", + "joi": "^17.9.2", + "cors": "^2.8.5", + "helmet": "^7.0.0", + "express-rate-limit": "^6.8.1", + "winston": "^3.10.0", + "dotenv": "^16.3.1", + "uuid": "^9.0.0", + "morgan": "^1.10.0", + "compression": "^1.7.4", + "express-validator": "^7.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "reflect-metadata": "^0.1.13" + }, + "devDependencies": { + "@types/express": "^4.17.17", + "@types/node": "^20.4.2", + "@types/bcryptjs": "^2.4.2", + "@types/jsonwebtoken": "^9.0.2", + "@types/cors": "^2.8.13", + "@types/morgan": "^1.9.4", + "@types/compression": "^1.7.2", + "@types/uuid": "^9.0.2", + "@types/jest": "^29.5.3", + "@types/supertest": "^2.0.12", + "typescript": "^5.1.6", + "ts-node": "^10.9.1", + "ts-node-dev": "^2.0.0", + "jest": "^29.6.1", + "ts-jest": "^29.1.1", + "supertest": "^6.3.3", + "@typescript-eslint/eslint-plugin": "^6.2.0", + "@typescript-eslint/parser": "^6.2.0", + "eslint": "^8.45.0", + "prettier": "^3.0.0", + "rimraf": "^5.0.1", + "mongodb-memory-server": "^8.14.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "roots": ["/src"], + "testMatch": ["**/__tests__/**/*.test.ts"], + "transform": { + "^.+\.tsx?$": "ts-jest" + }, + "coverageDirectory": "coverage", + "collectCoverageFrom": [ + "src/**/*.ts", + "!src/server.ts", + "!src/**/*.d.ts" + ] + }, + "eslintConfig": { + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "@typescript-eslint/recommended" + ], + "env": { + "node": true, + "es2021": true, + "jest": true + }, + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/explicit-function-return-type": "warn", + "no-console": "warn", + "prefer-const": "error" + } + }, + "prettier": { + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 80, + "arrowParens": "avoid" + } +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/config/database.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/config/database.ts new file mode 100644 index 00000000..6142fd83 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/config/database.ts @@ -0,0 +1,165 @@ +import mongoose from 'mongoose'; +import logger from '../utils/logger'; + +/** + * Database connection configuration + */ +class Database { + private mongoURI: string; + private options: mongoose.ConnectOptions; + + constructor() { + this.mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/user-management-ts'; + this.options = { + maxPoolSize: 10, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 45000, + family: 4, + }; + } + + /** + * Connect to MongoDB + */ + public async connect(): Promise { + try { + await mongoose.connect(this.mongoURI, this.options); + logger.info('MongoDB connected successfully'); + + // Handle connection events + mongoose.connection.on('error', (err: Error) => { + logger.error('MongoDB connection error:', err); + }); + + mongoose.connection.on('disconnected', () => { + logger.warn('MongoDB disconnected'); + }); + + mongoose.connection.on('reconnected', () => { + logger.info('MongoDB reconnected'); + }); + + // Handle process termination + process.on('SIGINT', () => this.gracefulShutdown('SIGINT')); + process.on('SIGTERM', () => this.gracefulShutdown('SIGTERM')); + + } catch (error) { + logger.error('MongoDB connection failed:', error as Error); + process.exit(1); + } + } + + /** + * Disconnect from MongoDB + */ + public async disconnect(): Promise { + try { + await mongoose.disconnect(); + logger.info('MongoDB disconnected successfully'); + } catch (error) { + logger.error('MongoDB disconnection error:', error as Error); + } + } + + /** + * Graceful shutdown + */ + private async gracefulShutdown(signal: string): Promise { + logger.info(`Received ${signal}. Graceful shutdown...`); + try { + await this.disconnect(); + process.exit(0); + } catch (error) { + logger.error('Error during graceful shutdown:', error as Error); + process.exit(1); + } + } + + /** + * Get connection status + */ + public getConnectionStatus(): string { + const states: Record = { + 0: 'disconnected', + 1: 'connected', + 2: 'connecting', + 3: 'disconnecting', + }; + return states[mongoose.connection.readyState] || 'unknown'; + } + + /** + * Check if database is connected + */ + public isConnected(): boolean { + return mongoose.connection.readyState === 1; + } + + /** + * Drop database (for testing) + */ + public async dropDatabase(): Promise { + if (process.env.NODE_ENV === 'test') { + try { + await mongoose.connection.db.dropDatabase(); + logger.info('Test database dropped'); + } catch (error) { + logger.error('Error dropping test database:', error as Error); + } + } else { + logger.warn('Database drop attempted in non-test environment'); + } + } + + /** + * Get database statistics + */ + public async getStats(): Promise { + try { + const stats = await mongoose.connection.db.stats(); + return { + database: mongoose.connection.name, + collections: stats.collections, + dataSize: stats.dataSize, + storageSize: stats.storageSize, + indexes: stats.indexes, + indexSize: stats.indexSize, + objects: stats.objects, + }; + } catch (error) { + logger.error('Error getting database stats:', error as Error); + return null; + } + } + + /** + * Create indexes for performance + */ + public async createIndexes(): Promise { + try { + // This would be called after models are loaded + // Indexes are already defined in the model schemas + logger.info('Database indexes created'); + } catch (error) { + logger.error('Error creating indexes:', error as Error); + } + } + + /** + * Health check + */ + public async healthCheck(): Promise { + try { + await mongoose.connection.db.admin().ping(); + return true; + } catch (error) { + logger.error('Database health check failed:', error as Error); + return false; + } + } +} + +// Create singleton instance +const database = new Database(); + +export default database; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/middleware/rateLimiter.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/middleware/rateLimiter.ts new file mode 100644 index 00000000..68a9cbde --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/middleware/rateLimiter.ts @@ -0,0 +1,238 @@ +import rateLimit from 'express-rate-limit'; +import { Request, Response } from 'express'; +import { RateLimitError } from '../utils/errors'; +import logger from '../utils/logger'; +import { IApiResponse } from '../types/User'; + +/** + * General rate limiter + * Limits requests per IP address + */ +export const generalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: { + success: false, + message: 'Too many requests from this IP, please try again later.', + error: { + message: 'Too many requests from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + handler: (req: Request, res: Response) => { + logger.warn(`Rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many requests from this IP, please try again later.'); + const response: IApiResponse = { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }; + res.status(429).json(response); + }, +}); + +/** + * Authentication rate limiter + * Stricter limits for login attempts + */ +export const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 login attempts per windowMs + message: { + success: false, + message: 'Too many login attempts from this IP, please try again later.', + error: { + message: 'Too many login attempts from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req: Request, res: Response) => { + logger.warn(`Authentication rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many login attempts from this IP, please try again later.'); + const response: IApiResponse = { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }; + res.status(429).json(response); + }, +}); + +/** + * User creation rate limiter + * Moderate limits for user registration + */ +export const createUserLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 10, // limit each IP to 10 user creations per hour + message: { + success: false, + message: 'Too many accounts created from this IP, please try again later.', + error: { + message: 'Too many accounts created from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req: Request, res: Response) => { + logger.warn(`User creation rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many accounts created from this IP, please try again later.'); + const response: IApiResponse = { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }; + res.status(429).json(response); + }, +}); + +/** + * Password reset rate limiter + * Limits password reset attempts + */ +export const passwordResetLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 3, // limit each IP to 3 password reset attempts per hour + message: { + success: false, + message: 'Too many password reset attempts from this IP, please try again later.', + error: { + message: 'Too many password reset attempts from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req: Request, res: Response) => { + logger.warn(`Password reset rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many password reset attempts from this IP, please try again later.'); + const response: IApiResponse = { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }; + res.status(429).json(response); + }, +}); + +/** + * API documentation rate limiter + * Limits access to API documentation + */ +export const docsLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 50, // limit each IP to 50 documentation requests per windowMs + message: { + success: false, + message: 'Too many documentation requests from this IP, please try again later.', + error: { + message: 'Too many documentation requests from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req: Request, res: Response) => { + logger.warn(`Documentation rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many documentation requests from this IP, please try again later.'); + const response: IApiResponse = { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }; + res.status(429).json(response); + }, +}); + +/** + * Export rate limiter + * Limits data export requests + */ +export const exportLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 5, // limit each IP to 5 export requests per hour + message: { + success: false, + message: 'Too many export requests from this IP, please try again later.', + error: { + message: 'Too many export requests from this IP, please try again later.', + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req: Request, res: Response) => { + logger.warn(`Export rate limit exceeded for IP: ${req.ip}`); + const error = new RateLimitError('Too many export requests from this IP, please try again later.'); + const response: IApiResponse = { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }; + res.status(429).json(response); + }, +}); + +/** + * Create custom rate limiter + */ +export const createCustomLimiter = (options: { + windowMs: number; + max: number; + message: string; + skipSuccessfulRequests?: boolean; + skipFailedRequests?: boolean; +}) => { + return rateLimit({ + windowMs: options.windowMs, + max: options.max, + message: { + success: false, + message: options.message, + error: { + message: options.message, + statusCode: 429, + }, + }, + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: options.skipSuccessfulRequests || false, + skipFailedRequests: options.skipFailedRequests || false, + handler: (req: Request, res: Response) => { + logger.warn(`Custom rate limit exceeded for IP: ${req.ip} - ${options.message}`); + const error = new RateLimitError(options.message); + const response: IApiResponse = { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode, + }, + }; + res.status(429).json(response); + }, + }); +}; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/models/User.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/models/User.ts new file mode 100644 index 00000000..c7137a09 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/models/User.ts @@ -0,0 +1,389 @@ +import mongoose, { Schema, Model } from 'mongoose'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import { v4 as uuidv4 } from 'uuid'; + +import { + IUser, + IUserDocument, + IUserResponse, + IUserStats, + UserRole, + UserStatus, + IJWTPayload, +} from '../types/User'; + +// User schema definition +const userSchema = new Schema( + { + id: { + type: String, + default: uuidv4, + unique: true, + required: true, + }, + username: { + type: String, + required: [true, 'Username is required'], + unique: true, + minlength: [3, 'Username must be at least 3 characters'], + maxlength: [20, 'Username cannot exceed 20 characters'], + match: [ + /^[a-zA-Z0-9_]+$/, + 'Username can only contain letters, numbers, and underscores', + ], + }, + email: { + type: String, + unique: true, + sparse: true, + match: [ + /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, + 'Please enter a valid email', + ], + }, + name: { + type: String, + required: [true, 'Name is required'], + minlength: [1, 'Name is required'], + maxlength: [100, 'Name cannot exceed 100 characters'], + }, + age: { + type: Number, + min: [0, 'Age cannot be negative'], + max: [150, 'Age cannot exceed 150'], + }, + password: { + type: String, + required: [true, 'Password is required'], + minlength: [8, 'Password must be at least 8 characters'], + select: false, + }, + role: { + type: String, + enum: Object.values(UserRole), + default: UserRole.USER, + }, + status: { + type: String, + enum: Object.values(UserStatus), + default: UserStatus.ACTIVE, + }, + lastLogin: { + type: Date, + default: null, + }, + loginAttempts: { + type: Number, + default: 0, + }, + permissions: { + type: [String], + default: [], + }, + metadata: { + type: Schema.Types.Mixed, + default: {}, + }, + }, + { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true }, + } +); + +// Virtual for user response (without sensitive data) +userSchema.virtual('response').get(function (this: IUserDocument): IUserResponse { + return { + id: this.id, + username: this.username, + email: this.email, + name: this.name, + age: this.age, + role: this.role, + status: this.status, + lastLogin: this.lastLogin, + permissions: this.permissions, + metadata: this.metadata, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; +}); + +// Virtual for checking if user is active +userSchema.virtual('isActive').get(function (this: IUserDocument): boolean { + return this.status === UserStatus.ACTIVE; +}); + +// Virtual for checking if user is admin +userSchema.virtual('isAdmin').get(function (this: IUserDocument): boolean { + return this.role === UserRole.ADMIN; +}); + +// Virtual for checking if user is locked +userSchema.virtual('isLocked').get(function (this: IUserDocument): boolean { + return this.loginAttempts >= 5 || this.status === UserStatus.SUSPENDED; +}); + +// Pre-save middleware to hash password +userSchema.pre('save', async function (this: IUserDocument, next) { + if (!this.isModified('password')) return next(); + + try { + const saltRounds = parseInt(process.env.BCRYPT_SALT_ROUNDS || '12', 10); + const hashedPassword = await bcrypt.hash(this.password, saltRounds); + this.password = hashedPassword; + next(); + } catch (error) { + next(error as Error); + } +}); + +// Instance method to check password +userSchema.methods.checkPassword = async function ( + this: IUserDocument, + candidatePassword: string +): Promise { + return await bcrypt.compare(candidatePassword, this.password); +}; + +// Instance method to generate JWT token +userSchema.methods.generateToken = function (this: IUserDocument): string { + const payload: IJWTPayload = { + id: this.id, + username: this.username, + role: this.role, + }; + + return jwt.sign(payload, process.env.JWT_SECRET || 'fallback-secret', { + expiresIn: process.env.JWT_EXPIRES_IN || '24h', + issuer: 'user-management-system', + }); +}; + +// Instance method to add permission +userSchema.methods.addPermission = function ( + this: IUserDocument, + permission: string +): void { + if (!this.permissions.includes(permission)) { + this.permissions.push(permission); + } +}; + +// Instance method to remove permission +userSchema.methods.removePermission = function ( + this: IUserDocument, + permission: string +): void { + this.permissions = this.permissions.filter(p => p !== permission); +}; + +// Instance method to check permission +userSchema.methods.hasPermission = function ( + this: IUserDocument, + permission: string +): boolean { + return this.permissions.includes(permission); +}; + +// Instance method to record successful login +userSchema.methods.recordLogin = function (this: IUserDocument): void { + this.lastLogin = new Date(); + this.loginAttempts = 0; +}; + +// Instance method to record failed login attempt +userSchema.methods.recordFailedLogin = function (this: IUserDocument): void { + this.loginAttempts += 1; + if (this.loginAttempts >= 5) { + this.status = UserStatus.SUSPENDED; + } +}; + +// Instance method to reset login attempts +userSchema.methods.resetLoginAttempts = function (this: IUserDocument): void { + this.loginAttempts = 0; +}; + +// Instance method to activate user +userSchema.methods.activate = function (this: IUserDocument): void { + this.status = UserStatus.ACTIVE; + this.loginAttempts = 0; +}; + +// Instance method to deactivate user +userSchema.methods.deactivate = function (this: IUserDocument): void { + this.status = UserStatus.INACTIVE; +}; + +// Instance method to suspend user +userSchema.methods.suspend = function (this: IUserDocument): void { + this.status = UserStatus.SUSPENDED; +}; + +// Instance method to delete user (soft delete) +userSchema.methods.delete = function (this: IUserDocument): void { + this.status = UserStatus.DELETED; +}; + +// Instance method to get metadata +userSchema.methods.getMetadata = function ( + this: IUserDocument, + key: string, + defaultValue: any = null +): any { + return this.metadata[key] || defaultValue; +}; + +// Instance method to set metadata +userSchema.methods.setMetadata = function ( + this: IUserDocument, + key: string, + value: any +): void { + this.metadata[key] = value; +}; + +// Instance method to remove metadata +userSchema.methods.removeMetadata = function ( + this: IUserDocument, + key: string +): void { + delete this.metadata[key]; +}; + +// Instance method to validate user data +userSchema.methods.validateUser = function (this: IUserDocument): string[] { + const errors: string[] = []; + + if (!this.username || this.username.length < 3) { + errors.push('Username must be at least 3 characters'); + } + + if (!this.name || this.name.length === 0) { + errors.push('Name is required'); + } + + if (this.age && (this.age < 0 || this.age > 150)) { + errors.push('Age must be between 0 and 150'); + } + + if ( + this.email && + !this.email.match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/) + ) { + errors.push('Email format is invalid'); + } + + return errors; +}; + +// Static method to find by username +userSchema.statics.findByUsername = function ( + this: Model, + username: string +) { + return this.findOne({ username }); +}; + +// Static method to find by email +userSchema.statics.findByEmail = function ( + this: Model, + email: string +) { + return this.findOne({ email }); +}; + +// Static method to find active users +userSchema.statics.findActive = function (this: Model) { + return this.find({ status: UserStatus.ACTIVE }); +}; + +// Static method to find by role +userSchema.statics.findByRole = function ( + this: Model, + role: UserRole +) { + return this.find({ role }); +}; + +// Static method to search users +userSchema.statics.searchUsers = function ( + this: Model, + query: string, + options: any = {} +) { + const searchRegex = new RegExp(query, 'i'); + const searchQuery = { + $or: [ + { username: searchRegex }, + { name: searchRegex }, + { email: searchRegex }, + ], + }; + + return this.find(searchQuery, null, options); +}; + +// Static method to get user statistics +userSchema.statics.getUserStats = async function ( + this: Model +): Promise { + const stats = await this.aggregate([ + { + $group: { + _id: null, + total: { $sum: 1 }, + active: { + $sum: { $cond: [{ $eq: ['$status', UserStatus.ACTIVE] }, 1, 0] }, + }, + admin: { + $sum: { $cond: [{ $eq: ['$role', UserRole.ADMIN] }, 1, 0] }, + }, + user: { + $sum: { $cond: [{ $eq: ['$role', UserRole.USER] }, 1, 0] }, + }, + guest: { + $sum: { $cond: [{ $eq: ['$role', UserRole.GUEST] }, 1, 0] }, + }, + withEmail: { $sum: { $cond: [{ $ne: ['$email', null] }, 1, 0] } }, + }, + }, + ]); + + return ( + stats[0] || { + total: 0, + active: 0, + admin: 0, + user: 0, + guest: 0, + withEmail: 0, + } + ); +}; + +// Indexes for performance +userSchema.index({ username: 1 }); +userSchema.index({ email: 1 }); +userSchema.index({ role: 1 }); +userSchema.index({ status: 1 }); +userSchema.index({ createdAt: -1 }); + +// Interface for the model +interface IUserModel extends Model { + findByUsername(username: string): Promise; + findByEmail(email: string): Promise; + findActive(): Promise; + findByRole(role: UserRole): Promise; + searchUsers(query: string, options?: any): Promise; + getUserStats(): Promise; +} + +// Export model and types +const User = mongoose.model('User', userSchema); + +export { User, UserRole, UserStatus }; +export default User; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/routes/userRoutes.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/routes/userRoutes.ts new file mode 100644 index 00000000..9d29498d --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/routes/userRoutes.ts @@ -0,0 +1,364 @@ +import express from 'express'; +import { body, param, query, ValidationChain } from 'express-validator'; +import { UserService } from '../services/UserService'; +import { UserRole } from '../types/User'; +import { asyncHandler, createSuccessResponse } from '../utils/errors'; +import { authLimiter, createUserLimiter, exportLimiter } from '../middleware/rateLimiter'; +import logger from '../utils/logger'; + +const router = express.Router(); +const userService = new UserService(); + +// User creation validation +const createUserValidation: ValidationChain[] = [ + body('username') + .isLength({ min: 3, max: 20 }) + .withMessage('Username must be between 3 and 20 characters') + .matches(/^[a-zA-Z0-9_]+$/) + .withMessage('Username can only contain letters, numbers, and underscores'), + body('name') + .isLength({ min: 1, max: 100 }) + .withMessage('Name must be between 1 and 100 characters'), + body('email') + .optional() + .isEmail() + .withMessage('Please provide a valid email address'), + body('age') + .optional() + .isInt({ min: 0, max: 150 }) + .withMessage('Age must be between 0 and 150'), + body('password') + .isLength({ min: 8 }) + .withMessage('Password must be at least 8 characters long'), + body('role') + .optional() + .isIn(Object.values(UserRole)) + .withMessage('Role must be admin, user, or guest'), +]; + +// User update validation +const updateUserValidation: ValidationChain[] = [ + param('id').notEmpty().withMessage('User ID is required'), + body('username') + .optional() + .isLength({ min: 3, max: 20 }) + .withMessage('Username must be between 3 and 20 characters') + .matches(/^[a-zA-Z0-9_]+$/) + .withMessage('Username can only contain letters, numbers, and underscores'), + body('name') + .optional() + .isLength({ min: 1, max: 100 }) + .withMessage('Name must be between 1 and 100 characters'), + body('email') + .optional() + .isEmail() + .withMessage('Please provide a valid email address'), + body('age') + .optional() + .isInt({ min: 0, max: 150 }) + .withMessage('Age must be between 0 and 150'), + body('role') + .optional() + .isIn(Object.values(UserRole)) + .withMessage('Role must be admin, user, or guest'), +]; + +// Password change validation +const passwordChangeValidation: ValidationChain[] = [ + param('id').notEmpty().withMessage('User ID is required'), + body('currentPassword') + .isLength({ min: 1 }) + .withMessage('Current password is required'), + body('newPassword') + .isLength({ min: 8 }) + .withMessage('New password must be at least 8 characters long'), +]; + +// Authentication validation +const authValidation: ValidationChain[] = [ + body('username') + .isLength({ min: 3 }) + .withMessage('Username must be at least 3 characters'), + body('password') + .isLength({ min: 1 }) + .withMessage('Password is required'), +]; + +// Search validation +const searchValidation: ValidationChain[] = [ + query('q') + .isLength({ min: 1 }) + .withMessage('Search query is required'), + query('page') + .optional() + .isInt({ min: 1 }) + .withMessage('Page must be a positive integer'), + query('limit') + .optional() + .isInt({ min: 1, max: 100 }) + .withMessage('Limit must be between 1 and 100'), +]; + +// Validation middleware +const validate = (req: express.Request, res: express.Response, next: express.NextFunction) => { + // This would typically use express-validator's validationResult + // For brevity, we'll assume validation passes + next(); +}; + +// Authentication middleware (simplified) +const auth = (req: express.Request, res: express.Response, next: express.NextFunction) => { + // This would typically verify JWT token + // For brevity, we'll assume authentication passes + next(); +}; + +// @route POST /api/users +// @desc Create a new user +// @access Public +router.post( + '/', + createUserLimiter, + createUserValidation, + validate, + asyncHandler(async (req: express.Request, res: express.Response) => { + const user = await userService.createUser(req.body); + logger.info(`User created via API: ${user.username}`); + res.status(201).json(createSuccessResponse(user, 'User created successfully')); + }) +); + +// @route POST /api/users/auth +// @desc Authenticate user +// @access Public +router.post( + '/auth', + authLimiter, + authValidation, + validate, + asyncHandler(async (req: express.Request, res: express.Response) => { + const { username, password } = req.body; + const result = await userService.authenticateUser(username, password); + logger.info(`User authenticated via API: ${username}`); + res.json(createSuccessResponse(result, 'Authentication successful')); + }) +); + +// @route GET /api/users +// @desc Get all users with pagination +// @access Private (Admin only) +router.get( + '/', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 20; + const filter: any = {}; + + // Add filtering by role if provided + if (req.query.role) { + filter.role = req.query.role; + } + + // Add filtering by status if provided + if (req.query.status) { + filter.status = req.query.status; + } + + const result = await userService.getAllUsers(page, limit, filter); + res.json(createSuccessResponse(result, 'Users retrieved successfully')); + }) +); + +// @route GET /api/users/search +// @desc Search users +// @access Private +router.get( + '/search', + auth, + searchValidation, + validate, + asyncHandler(async (req: express.Request, res: express.Response) => { + const query = req.query.q as string; + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 20; + + const result = await userService.searchUsers(query, page, limit); + res.json(createSuccessResponse(result, 'Search completed successfully')); + }) +); + +// @route GET /api/users/stats +// @desc Get user statistics +// @access Private (Admin only) +router.get( + '/stats', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const stats = await userService.getUserStats(); + res.json(createSuccessResponse(stats, 'Statistics retrieved successfully')); + }) +); + +// @route GET /api/users/export +// @desc Export all users +// @access Private (Admin only) +router.get( + '/export', + auth, + exportLimiter, + asyncHandler(async (req: express.Request, res: express.Response) => { + const users = await userService.exportUsers(); + res.json(createSuccessResponse(users, 'Users exported successfully')); + }) +); + +// @route GET /api/users/active +// @desc Get active users +// @access Private +router.get( + '/active', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const users = await userService.getActiveUsers(); + res.json(createSuccessResponse(users, 'Active users retrieved successfully')); + }) +); + +// @route GET /api/users/role/:role +// @desc Get users by role +// @access Private (Admin only) +router.get( + '/role/:role', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const role = req.params.role as UserRole; + const users = await userService.getUsersByRole(role); + res.json(createSuccessResponse(users, `Users with role ${role} retrieved successfully`)); + }) +); + +// @route GET /api/users/:id +// @desc Get user by ID +// @access Private +router.get( + '/:id', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const user = await userService.getUserById(req.params.id); + res.json(createSuccessResponse(user, 'User retrieved successfully')); + }) +); + +// @route GET /api/users/:id/activity +// @desc Get user activity +// @access Private (Admin or same user) +router.get( + '/:id/activity', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const activity = await userService.getUserActivity(req.params.id); + res.json(createSuccessResponse(activity, 'User activity retrieved successfully')); + }) +); + +// @route PUT /api/users/:id +// @desc Update user +// @access Private (Admin or same user) +router.put( + '/:id', + auth, + updateUserValidation, + validate, + asyncHandler(async (req: express.Request, res: express.Response) => { + const user = await userService.updateUser(req.params.id, req.body); + logger.info(`User updated via API: ${user.username}`); + res.json(createSuccessResponse(user, 'User updated successfully')); + }) +); + +// @route PUT /api/users/:id/password +// @desc Change user password +// @access Private (Admin or same user) +router.put( + '/:id/password', + auth, + passwordChangeValidation, + validate, + asyncHandler(async (req: express.Request, res: express.Response) => { + const { currentPassword, newPassword } = req.body; + await userService.changePassword(req.params.id, currentPassword, newPassword); + logger.info(`Password changed via API for user: ${req.params.id}`); + res.json(createSuccessResponse(null, 'Password changed successfully')); + }) +); + +// @route PUT /api/users/:id/reset-password +// @desc Reset user password (Admin only) +// @access Private (Admin only) +router.put( + '/:id/reset-password', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const { newPassword } = req.body; + await userService.resetPassword(req.params.id, newPassword); + logger.info(`Password reset via API for user: ${req.params.id}`); + res.json(createSuccessResponse(null, 'Password reset successfully')); + }) +); + +// @route PUT /api/users/:id/permissions +// @desc Add permission to user +// @access Private (Admin only) +router.put( + '/:id/permissions', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const { permission } = req.body; + await userService.addPermission(req.params.id, permission); + logger.info(`Permission added via API for user: ${req.params.id}`); + res.json(createSuccessResponse(null, 'Permission added successfully')); + }) +); + +// @route DELETE /api/users/:id/permissions +// @desc Remove permission from user +// @access Private (Admin only) +router.delete( + '/:id/permissions', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + const { permission } = req.body; + await userService.removePermission(req.params.id, permission); + logger.info(`Permission removed via API for user: ${req.params.id}`); + res.json(createSuccessResponse(null, 'Permission removed successfully')); + }) +); + +// @route DELETE /api/users/:id +// @desc Delete user (soft delete) +// @access Private (Admin only) +router.delete( + '/:id', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + await userService.deleteUser(req.params.id); + logger.info(`User deleted via API: ${req.params.id}`); + res.json(createSuccessResponse(null, 'User deleted successfully')); + }) +); + +// @route DELETE /api/users/:id/hard +// @desc Hard delete user (permanent) +// @access Private (Admin only) +router.delete( + '/:id/hard', + auth, + asyncHandler(async (req: express.Request, res: express.Response) => { + await userService.hardDeleteUser(req.params.id); + logger.info(`User permanently deleted via API: ${req.params.id}`); + res.json(createSuccessResponse(null, 'User permanently deleted')); + }) +); + +export default router; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/server.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/server.ts new file mode 100644 index 00000000..2150f834 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/server.ts @@ -0,0 +1,215 @@ +import 'reflect-metadata'; +import dotenv from 'dotenv'; +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import compression from 'compression'; +import morgan from 'morgan'; + +import database from './config/database'; +import userRoutes from './routes/userRoutes'; +import { generalLimiter } from './middleware/rateLimiter'; +import { globalErrorHandler, handleNotFound } from './utils/errors'; +import logger from './utils/logger'; +import { IApiResponse } from './types/User'; + +// Load environment variables +dotenv.config(); + +/** + * Express application setup + */ +class App { + public app: express.Application; + private port: number; + private server?: any; + + constructor() { + this.app = express(); + this.port = parseInt(process.env.PORT || '3000', 10); + this.setupMiddleware(); + this.setupRoutes(); + this.setupErrorHandling(); + } + + /** + * Setup middleware + */ + private setupMiddleware(): void { + // Security middleware + this.app.use(helmet()); + + // CORS configuration + this.app.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'], + credentials: true, + })); + + // Compression middleware + this.app.use(compression()); + + // Body parsing middleware + this.app.use(express.json({ limit: '10mb' })); + this.app.use(express.urlencoded({ extended: true, limit: '10mb' })); + + // Logging middleware + this.app.use(morgan('combined', { stream: logger.stream })); + + // Rate limiting + this.app.use(generalLimiter); + + // Request ID middleware + this.app.use((req, res, next) => { + (req as any).requestId = Math.random().toString(36).substr(2, 9); + res.set('X-Request-ID', (req as any).requestId); + next(); + }); + + // Health check endpoint + this.app.get('/health', (req, res) => { + const healthResponse: IApiResponse = { + success: true, + message: 'Service is healthy', + data: { + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + database: database.getConnectionStatus(), + version: process.env.npm_package_version || '1.0.0', + environment: process.env.NODE_ENV || 'development', + }, + }; + res.json(healthResponse); + }); + } + + /** + * Setup routes + */ + private setupRoutes(): void { + // API routes + this.app.use('/api/users', userRoutes); + + // Root endpoint + this.app.get('/', (req, res) => { + const rootResponse: IApiResponse = { + success: true, + message: 'User Management API - TypeScript Edition', + data: { + name: 'User Management API', + version: '1.0.0', + language: 'TypeScript', + endpoints: { + health: '/health', + users: '/api/users', + auth: '/api/users/auth', + docs: '/api/docs', + }, + }, + }; + res.json(rootResponse); + }); + } + + /** + * Setup error handling + */ + private setupErrorHandling(): void { + // Handle 404 for unknown routes + this.app.use(handleNotFound); + + // Global error handler + this.app.use(globalErrorHandler); + } + + /** + * Start the server + */ + public async start(): Promise { + try { + // Connect to database + await database.connect(); + + // Start server + this.server = this.app.listen(this.port, () => { + logger.info(`Server running on port ${this.port}`); + logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`); + logger.info(`Health check: http://localhost:${this.port}/health`); + logger.info(`API documentation: http://localhost:${this.port}/api/docs`); + }); + + // Handle server errors + this.server.on('error', (error: any) => { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof this.port === 'string' ? `Pipe ${this.port}` : `Port ${this.port}`; + + switch (error.code) { + case 'EACCES': + logger.error(`${bind} requires elevated privileges`); + process.exit(1); + break; + case 'EADDRINUSE': + logger.error(`${bind} is already in use`); + process.exit(1); + break; + default: + throw error; + } + }); + + // Graceful shutdown + process.on('SIGTERM', () => this.gracefulShutdown('SIGTERM')); + process.on('SIGINT', () => this.gracefulShutdown('SIGINT')); + + } catch (error) { + logger.error('Failed to start server:', error as Error); + process.exit(1); + } + } + + /** + * Graceful shutdown + */ + private async gracefulShutdown(signal: string): Promise { + logger.info(`Received ${signal}. Graceful shutdown...`); + + if (this.server) { + this.server.close(async () => { + logger.info('HTTP server closed'); + + try { + await database.disconnect(); + logger.info('Database disconnected'); + process.exit(0); + } catch (error) { + logger.error('Error during graceful shutdown:', error as Error); + process.exit(1); + } + }); + } + } + + /** + * Get Express app instance + */ + public getApp(): express.Application { + return this.app; + } +} + +// Create and start the application +const app = new App(); + +// Start server if not in test environment +if (process.env.NODE_ENV !== 'test') { + app.start().catch(error => { + logger.error('Failed to start application:', error as Error); + process.exit(1); + }); +} + +// Export for testing +export default app.getApp(); \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/services/UserService.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/services/UserService.ts new file mode 100644 index 00000000..962d3003 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/services/UserService.ts @@ -0,0 +1,518 @@ +import { User } from '../models/User'; +import { + IUser, + IUserResponse, + ICreateUser, + IUpdateUser, + IUserStats, + IAuthResult, + IUserActivity, + IPaginatedUsers, + ISearchUsersResponse, + IUserFilter, + UserRole, +} from '../types/User'; +import { AppError } from '../utils/errors'; +import logger from '../utils/logger'; + +/** + * UserService class handles all user-related business logic + */ +export class UserService { + /** + * Create a new user + * @param userData - User data object + * @returns Created user response + */ + async createUser(userData: ICreateUser): Promise { + try { + // Check if username already exists + const existingUsername = await User.findByUsername(userData.username); + if (existingUsername) { + throw new AppError('Username already exists', 400); + } + + // Check if email already exists (if provided) + if (userData.email) { + const existingEmail = await User.findByEmail(userData.email); + if (existingEmail) { + throw new AppError('Email already exists', 400); + } + } + + // Create new user + const user = new User(userData); + + // Validate user data + const validationErrors = user.validateUser(); + if (validationErrors.length > 0) { + throw new AppError(validationErrors.join(', '), 400); + } + + await user.save(); + + logger.info(`User created successfully: ${user.username}`); + return user.response; + } catch (error) { + logger.error('Error creating user:', error as Error); + throw error; + } + } + + /** + * Get user by ID + * @param id - User ID + * @returns User response + */ + async getUserById(id: string): Promise { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + return user.response; + } catch (error) { + logger.error('Error getting user by ID:', error as Error); + throw error; + } + } + + /** + * Get user by username + * @param username - Username + * @returns User response + */ + async getUserByUsername(username: string): Promise { + try { + const user = await User.findByUsername(username); + if (!user) { + throw new AppError('User not found', 404); + } + return user.response; + } catch (error) { + logger.error('Error getting user by username:', error as Error); + throw error; + } + } + + /** + * Get user by email + * @param email - Email address + * @returns User response + */ + async getUserByEmail(email: string): Promise { + try { + const user = await User.findByEmail(email); + if (!user) { + throw new AppError('User not found', 404); + } + return user.response; + } catch (error) { + logger.error('Error getting user by email:', error as Error); + throw error; + } + } + + /** + * Update user + * @param id - User ID + * @param updateData - Update data + * @returns Updated user response + */ + async updateUser(id: string, updateData: IUpdateUser): Promise { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + // Apply updates (excluding password and id) + Object.keys(updateData).forEach(key => { + if (key !== 'password' && key !== 'id') { + (user as any)[key] = (updateData as any)[key]; + } + }); + + // Validate updated data + const validationErrors = user.validateUser(); + if (validationErrors.length > 0) { + throw new AppError(validationErrors.join(', '), 400); + } + + await user.save(); + + logger.info(`User updated successfully: ${user.username}`); + return user.response; + } catch (error) { + logger.error('Error updating user:', error as Error); + throw error; + } + } + + /** + * Delete user (soft delete) + * @param id - User ID + * @returns Success status + */ + async deleteUser(id: string): Promise { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + user.delete(); + await user.save(); + + logger.info(`User deleted successfully: ${user.username}`); + return true; + } catch (error) { + logger.error('Error deleting user:', error as Error); + throw error; + } + } + + /** + * Hard delete user (permanent deletion) + * @param id - User ID + * @returns Success status + */ + async hardDeleteUser(id: string): Promise { + try { + const result = await User.deleteOne({ id }); + if (result.deletedCount === 0) { + throw new AppError('User not found', 404); + } + + logger.info(`User permanently deleted: ${id}`); + return true; + } catch (error) { + logger.error('Error hard deleting user:', error as Error); + throw error; + } + } + + /** + * Get all users with pagination + * @param page - Page number + * @param limit - Items per page + * @param filter - Filter criteria + * @returns Paginated users response + */ + async getAllUsers( + page: number = 1, + limit: number = 20, + filter: IUserFilter = {} + ): Promise { + try { + const skip = (page - 1) * limit; + + const users = await User.find(filter) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit); + + const total = await User.countDocuments(filter); + const totalPages = Math.ceil(total / limit); + + return { + users: users.map(user => user.response), + pagination: { + page, + limit, + total, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1, + }, + }; + } catch (error) { + logger.error('Error getting all users:', error as Error); + throw error; + } + } + + /** + * Get active users + * @returns Active users + */ + async getActiveUsers(): Promise { + try { + const users = await User.findActive(); + return users.map(user => user.response); + } catch (error) { + logger.error('Error getting active users:', error as Error); + throw error; + } + } + + /** + * Get users by role + * @param role - User role + * @returns Users with specified role + */ + async getUsersByRole(role: UserRole): Promise { + try { + const users = await User.findByRole(role); + return users.map(user => user.response); + } catch (error) { + logger.error('Error getting users by role:', error as Error); + throw error; + } + } + + /** + * Search users + * @param query - Search query + * @param page - Page number + * @param limit - Items per page + * @returns Search results + */ + async searchUsers( + query: string, + page: number = 1, + limit: number = 20 + ): Promise { + try { + const skip = (page - 1) * limit; + + const users = await User.searchUsers(query, { + skip, + limit, + sort: { createdAt: -1 }, + }); + + // Count total matching users + const totalUsers = await User.searchUsers(query); + const total = totalUsers.length; + const totalPages = Math.ceil(total / limit); + + return { + users: users.map(user => user.response), + query, + pagination: { + page, + limit, + total, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1, + }, + }; + } catch (error) { + logger.error('Error searching users:', error as Error); + throw error; + } + } + + /** + * Get user statistics + * @returns User statistics + */ + async getUserStats(): Promise { + try { + const stats = await User.getUserStats(); + return stats; + } catch (error) { + logger.error('Error getting user statistics:', error as Error); + throw error; + } + } + + /** + * Authenticate user + * @param username - Username + * @param password - Password + * @returns Authentication result + */ + async authenticateUser(username: string, password: string): Promise { + try { + const user = await User.findByUsername(username).select('+password'); + + if (!user || !(await user.checkPassword(password))) { + // Record failed login attempt if user exists + if (user) { + user.recordFailedLogin(); + await user.save(); + } + throw new AppError('Invalid username or password', 401); + } + + if (!user.isActive) { + throw new AppError('User account is not active', 401); + } + + if (user.isLocked) { + throw new AppError('User account is locked', 401); + } + + // Record successful login + user.recordLogin(); + await user.save(); + + // Generate token + const token = user.generateToken(); + + logger.info(`User authenticated successfully: ${user.username}`); + + return { + user: user.response, + token, + }; + } catch (error) { + logger.error('Error authenticating user:', error as Error); + throw error; + } + } + + /** + * Change user password + * @param id - User ID + * @param currentPassword - Current password + * @param newPassword - New password + * @returns Success status + */ + async changePassword( + id: string, + currentPassword: string, + newPassword: string + ): Promise { + try { + const user = await User.findOne({ id }).select('+password'); + if (!user) { + throw new AppError('User not found', 404); + } + + if (!(await user.checkPassword(currentPassword))) { + throw new AppError('Current password is incorrect', 400); + } + + user.password = newPassword; + await user.save(); + + logger.info(`Password changed successfully for user: ${user.username}`); + return true; + } catch (error) { + logger.error('Error changing password:', error as Error); + throw error; + } + } + + /** + * Reset user password (admin function) + * @param id - User ID + * @param newPassword - New password + * @returns Success status + */ + async resetPassword(id: string, newPassword: string): Promise { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + user.password = newPassword; + user.resetLoginAttempts(); + await user.save(); + + logger.info(`Password reset successfully for user: ${user.username}`); + return true; + } catch (error) { + logger.error('Error resetting password:', error as Error); + throw error; + } + } + + /** + * Add permission to user + * @param id - User ID + * @param permission - Permission to add + * @returns Success status + */ + async addPermission(id: string, permission: string): Promise { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + user.addPermission(permission); + await user.save(); + + logger.info(`Permission added to user ${user.username}: ${permission}`); + return true; + } catch (error) { + logger.error('Error adding permission:', error as Error); + throw error; + } + } + + /** + * Remove permission from user + * @param id - User ID + * @param permission - Permission to remove + * @returns Success status + */ + async removePermission(id: string, permission: string): Promise { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + user.removePermission(permission); + await user.save(); + + logger.info(`Permission removed from user ${user.username}: ${permission}`); + return true; + } catch (error) { + logger.error('Error removing permission:', error as Error); + throw error; + } + } + + /** + * Export users data + * @returns Users data for export + */ + async exportUsers(): Promise { + try { + const users = await User.find().sort({ createdAt: -1 }); + return users.map(user => user.response); + } catch (error) { + logger.error('Error exporting users:', error as Error); + throw error; + } + } + + /** + * Get user activity + * @param id - User ID + * @returns User activity data + */ + async getUserActivity(id: string): Promise { + try { + const user = await User.findOne({ id }); + if (!user) { + throw new AppError('User not found', 404); + } + + return { + id: user.id, + username: user.username, + lastLogin: user.lastLogin, + loginAttempts: user.loginAttempts, + isActive: user.isActive, + isLocked: user.isLocked, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; + } catch (error) { + logger.error('Error getting user activity:', error as Error); + throw error; + } + } +} + +export default UserService; +// AUTO_REINDEX_MARKER: ci_auto_reindex_test_token_ts \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/types/User.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/types/User.ts new file mode 100644 index 00000000..3ee23ed4 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/types/User.ts @@ -0,0 +1,203 @@ +import { Document } from 'mongoose'; + +// User roles enumeration +export enum UserRole { + ADMIN = 'admin', + USER = 'user', + GUEST = 'guest', +} + +// User status enumeration +export enum UserStatus { + ACTIVE = 'active', + INACTIVE = 'inactive', + SUSPENDED = 'suspended', + DELETED = 'deleted', +} + +// Base user interface +export interface IUser { + id: string; + username: string; + email?: string; + name: string; + age?: number; + password: string; + role: UserRole; + status: UserStatus; + lastLogin?: Date; + loginAttempts: number; + permissions: string[]; + metadata: Record; + createdAt: Date; + updatedAt: Date; +} + +// User response interface (without sensitive data) +export interface IUserResponse { + id: string; + username: string; + email?: string; + name: string; + age?: number; + role: UserRole; + status: UserStatus; + lastLogin?: Date; + permissions: string[]; + metadata: Record; + createdAt: Date; + updatedAt: Date; +} + +// User creation interface +export interface ICreateUser { + username: string; + email?: string; + name: string; + age?: number; + password: string; + role?: UserRole; + status?: UserStatus; + permissions?: string[]; + metadata?: Record; +} + +// User update interface +export interface IUpdateUser { + username?: string; + email?: string; + name?: string; + age?: number; + role?: UserRole; + status?: UserStatus; + permissions?: string[]; + metadata?: Record; +} + +// User document interface (extends Mongoose Document) +export interface IUserDocument extends IUser, Document { + // Virtual properties + isActive: boolean; + isAdmin: boolean; + isLocked: boolean; + response: IUserResponse; + + // Instance methods + checkPassword(candidatePassword: string): Promise; + generateToken(): string; + addPermission(permission: string): void; + removePermission(permission: string): void; + hasPermission(permission: string): boolean; + recordLogin(): void; + recordFailedLogin(): void; + resetLoginAttempts(): void; + activate(): void; + deactivate(): void; + suspend(): void; + delete(): void; + getMetadata(key: string, defaultValue?: any): any; + setMetadata(key: string, value: any): void; + removeMetadata(key: string): void; + validateUser(): string[]; +} + +// User statistics interface +export interface IUserStats { + total: number; + active: number; + admin: number; + user: number; + guest: number; + withEmail: number; +} + +// Authentication result interface +export interface IAuthResult { + user: IUserResponse; + token: string; +} + +// User activity interface +export interface IUserActivity { + id: string; + username: string; + lastLogin?: Date; + loginAttempts: number; + isActive: boolean; + isLocked: boolean; + createdAt: Date; + updatedAt: Date; +} + +// Pagination interface +export interface IPagination { + page: number; + limit: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; +} + +// Paginated users response +export interface IPaginatedUsers { + users: IUserResponse[]; + pagination: IPagination; +} + +// Search users response +export interface ISearchUsersResponse { + users: IUserResponse[]; + query: string; + pagination: IPagination; +} + +// Password change interface +export interface IPasswordChange { + currentPassword: string; + newPassword: string; +} + +// User filter interface +export interface IUserFilter { + role?: UserRole; + status?: UserStatus; + hasEmail?: boolean; + createdAfter?: Date; + createdBefore?: Date; +} + +// JWT payload interface +export interface IJWTPayload { + id: string; + username: string; + role: UserRole; + iat?: number; + exp?: number; +} + +// Request with user interface +export interface IAuthenticatedRequest extends Request { + user?: IUserDocument; + requestId?: string; +} + +// API response interface +export interface IApiResponse { + success: boolean; + message: string; + data?: T; + error?: { + message: string; + statusCode: number; + errors?: any[]; + stack?: string; + }; +} + +// Validation error interface +export interface IValidationError { + field: string; + message: string; + value: any; +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/utils/errors.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/utils/errors.ts new file mode 100644 index 00000000..6aa27d70 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/utils/errors.ts @@ -0,0 +1,262 @@ +import { Request, Response, NextFunction } from 'express'; +import { IApiResponse, IValidationError } from '../types/User'; + +/** + * Base application error class + */ +export class AppError extends Error { + public statusCode: number; + public isOperational: boolean; + public status: string; + + constructor(message: string, statusCode: number = 500, isOperational: boolean = true) { + super(message); + + this.statusCode = statusCode; + this.isOperational = isOperational; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + + // Capture stack trace + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * Validation error class + */ +export class ValidationError extends AppError { + public errors: IValidationError[]; + + constructor(message: string, errors: IValidationError[] = []) { + super(message, 400); + this.errors = errors; + } +} + +/** + * Authentication error class + */ +export class AuthenticationError extends AppError { + constructor(message: string = 'Authentication failed') { + super(message, 401); + } +} + +/** + * Authorization error class + */ +export class AuthorizationError extends AppError { + constructor(message: string = 'Access denied') { + super(message, 403); + } +} + +/** + * Not found error class + */ +export class NotFoundError extends AppError { + constructor(message: string = 'Resource not found') { + super(message, 404); + } +} + +/** + * Conflict error class + */ +export class ConflictError extends AppError { + constructor(message: string = 'Resource conflict') { + super(message, 409); + } +} + +/** + * Rate limit error class + */ +export class RateLimitError extends AppError { + constructor(message: string = 'Too many requests') { + super(message, 429); + } +} + +/** + * Database error class + */ +export class DatabaseError extends AppError { + constructor(message: string = 'Database error') { + super(message, 500); + } +} + +/** + * External service error class + */ +export class ExternalServiceError extends AppError { + constructor(message: string = 'External service error') { + super(message, 502); + } +} + +/** + * Global error handler for Express + */ +export const globalErrorHandler = ( + err: any, + req: Request, + res: Response, + next: NextFunction +): void => { + // Default error values + let error = { ...err }; + error.message = err.message; + + // Log error + console.error('Error:', err); + + // Mongoose bad ObjectId + if (err.name === 'CastError') { + const message = 'Resource not found'; + error = new NotFoundError(message); + } + + // Mongoose duplicate key + if (err.code === 11000) { + const value = err.errmsg?.match(/(["'])(\\?.)*?\1/)?.[0] || 'unknown'; + const message = `Duplicate field value: ${value}. Please use another value`; + error = new ConflictError(message); + } + + // Mongoose validation error + if (err.name === 'ValidationError') { + const errors: IValidationError[] = Object.values(err.errors).map( + (val: any) => ({ + field: val.path, + message: val.message, + value: val.value, + }) + ); + error = new ValidationError('Validation failed', errors); + } + + // JWT errors + if (err.name === 'JsonWebTokenError') { + error = new AuthenticationError('Invalid token'); + } + + if (err.name === 'TokenExpiredError') { + error = new AuthenticationError('Token expired'); + } + + // Send error response + const response: IApiResponse = { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode || 500, + ...(process.env.NODE_ENV === 'development' && { stack: error.stack }), + ...(error.errors && { errors: error.errors }), + }, + }; + + res.status(error.statusCode || 500).json(response); +}; + +/** + * Async error handler wrapper + */ +export const asyncHandler = ( + fn: (req: Request, res: Response, next: NextFunction) => Promise +) => { + return (req: Request, res: Response, next: NextFunction): void => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +}; + +/** + * Create error response + */ +export const createErrorResponse = ( + message: string, + statusCode: number = 500, + errors: IValidationError[] | null = null +): IApiResponse => { + const response: IApiResponse = { + success: false, + message, + error: { + message, + statusCode, + }, + }; + + if (errors) { + response.error!.errors = errors; + } + + return response; +}; + +/** + * Create success response + */ +export const createSuccessResponse = ( + data: T, + message: string = 'Success' +): IApiResponse => { + return { + success: true, + message, + data, + }; +}; + +/** + * Handle 404 for unknown routes + */ +export const handleNotFound = ( + req: Request, + res: Response, + next: NextFunction +): void => { + const error = new NotFoundError(`Route ${req.originalUrl} not found`); + next(error); +}; + +/** + * Type guard for checking if error is operational + */ +export const isOperationalError = (error: any): error is AppError => { + return error instanceof AppError && error.isOperational; +}; + +/** + * Error logger utility + */ +export const logError = (error: Error, req?: Request): void => { + const timestamp = new Date().toISOString(); + const method = req?.method || 'UNKNOWN'; + const url = req?.originalUrl || 'UNKNOWN'; + const userAgent = req?.get('User-Agent') || 'UNKNOWN'; + const ip = req?.ip || 'UNKNOWN'; + + console.error(`[${timestamp}] ${method} ${url} - ${error.message}`); + console.error(`User-Agent: ${userAgent}`); + console.error(`IP: ${ip}`); + console.error(`Stack: ${error.stack}`); +}; + +/** + * Error response formatter + */ +export const formatErrorResponse = (error: AppError): IApiResponse => { + return { + success: false, + message: error.message, + error: { + message: error.message, + statusCode: error.statusCode, + ...(process.env.NODE_ENV === 'development' && { stack: error.stack }), + ...(error instanceof ValidationError && { errors: error.errors }), + }, + }; +}; \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/utils/logger.ts b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/utils/logger.ts new file mode 100644 index 00000000..67fa25f6 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/src/utils/logger.ts @@ -0,0 +1,191 @@ +import winston from 'winston'; + +// Define log levels +const levels = { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, +}; + +// Define colors for each level +const colors = { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'white', +}; + +// Tell winston about the colors +winston.addColors(colors); + +// Custom format function +const format = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), + winston.format.colorize({ all: true }), + winston.format.printf( + (info) => `${info.timestamp} ${info.level}: ${info.message}` + ) +); + +// Define which transports the logger must use +const transports: winston.transport[] = [ + // Console transport + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ), + }), + + // File transport for errors + new winston.transports.File({ + filename: 'logs/error.log', + level: 'error', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }), + + // File transport for all logs + new winston.transports.File({ + filename: 'logs/combined.log', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }), +]; + +// Create the logger +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + levels, + format, + transports, +}); + +// Create a stream object with a 'write' function that will be used by Morgan +interface LoggerStream { + write: (message: string) => void; +} + +const loggerStream: LoggerStream = { + write: (message: string) => { + // Remove the trailing newline + logger.http(message.trim()); + }, +}; + +// Add stream property to logger +(logger as any).stream = loggerStream; + +// Export logger with proper typing +interface Logger extends winston.Logger { + stream: LoggerStream; +} + +export default logger as Logger; + +// Export individual log level functions for convenience +export const logError = (message: string, error?: Error): void => { + if (error) { + logger.error(`${message}: ${error.message}`, { stack: error.stack }); + } else { + logger.error(message); + } +}; + +export const logWarn = (message: string): void => { + logger.warn(message); +}; + +export const logInfo = (message: string): void => { + logger.info(message); +}; + +export const logDebug = (message: string): void => { + logger.debug(message); +}; + +// Log HTTP requests +export const logHttp = (message: string): void => { + logger.http(message); +}; + +// Log with context +export const logWithContext = ( + level: string, + message: string, + context?: Record +): void => { + logger.log(level, message, context); +}; + +// Create child logger with additional context +export const createChildLogger = (context: Record): winston.Logger => { + return logger.child(context); +}; + +// Performance logging utility +export const logPerformance = (operation: string, startTime: number): void => { + const duration = Date.now() - startTime; + logger.info(`${operation} completed in ${duration}ms`); +}; + +// Database query logging +export const logQuery = (query: string, duration?: number): void => { + const message = duration + ? `Query executed in ${duration}ms: ${query}` + : `Query executed: ${query}`; + logger.debug(message); +}; + +// User action logging +export const logUserAction = ( + userId: string, + action: string, + details?: Record +): void => { + const message = `User ${userId} performed action: ${action}`; + logger.info(message, details); +}; + +// Security event logging +export const logSecurityEvent = ( + event: string, + details: Record +): void => { + logger.warn(`Security event: ${event}`, details); +}; + +// API request logging +export const logApiRequest = ( + method: string, + url: string, + statusCode: number, + duration: number, + userId?: string +): void => { + const message = `${method} ${url} - ${statusCode} (${duration}ms)`; + const context = userId ? { userId } : {}; + logger.http(message, context); +}; + +// Environment-specific logging configuration +if (process.env.NODE_ENV === 'production') { + // In production, reduce console logging + logger.remove(logger.transports[0]); + logger.add(new winston.transports.Console({ + level: 'warn', + format: winston.format.simple(), + })); +} + +if (process.env.NODE_ENV === 'test') { + // In test environment, minimize logging + logger.level = 'error'; +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/tsconfig.json b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/tsconfig.json new file mode 100644 index 00000000..60ba4021 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/typescript/user-management/tsconfig.json @@ -0,0 +1,49 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["node", "jest"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@/models/*": ["src/models/*"], + "@/services/*": ["src/services/*"], + "@/utils/*": ["src/utils/*"], + "@/middleware/*": ["src/middleware/*"], + "@/routes/*": ["src/routes/*"], + "@/config/*": ["src/config/*"], + "@/types/*": ["src/types/*"] + } + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "coverage", + "**/*.test.ts" + ] +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/build.zig b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/build.zig new file mode 100644 index 00000000..ea52f569 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/build.zig @@ -0,0 +1,156 @@ +const std = @import("std"); + +// Although this function looks imperative, it does not perform the build +// directly and instead it mutates the build graph (`b`) that will be then +// executed by an external runner. The functions in `std.Build` implement a DSL +// for defining build steps and express dependencies between them, allowing the +// build runner to parallelize the build automatically (and the cache system to +// know when a step doesn't need to be re-run). +pub fn build(b: *std.Build) void { + // Standard target options allow the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + // It's also possible to define more custom flags to toggle optional features + // of this build script using `b.option()`. All defined flags (including + // target and optimize options) will be listed when running `zig build --help` + // in this directory. + + // This creates a module, which represents a collection of source files alongside + // some compilation options, such as optimization mode and linked system libraries. + // Zig modules are the preferred way of making Zig code available to consumers. + // addModule defines a module that we intend to make available for importing + // to our consumers. We must give it a name because a Zig package can expose + // multiple modules and consumers will need to be able to specify which + // module they want to access. + const mod = b.addModule("code_index_example", .{ + // The root source file is the "entry point" of this module. Users of + // this module will only be able to access public declarations contained + // in this file, which means that if you have declarations that you + // intend to expose to consumers that were defined in other files part + // of this module, you will have to make sure to re-export them from + // the root file. + .root_source_file = b.path("src/root.zig"), + // Later on we'll use this module as the root module of a test executable + // which requires us to specify a target. + .target = target, + }); + + // Here we define an executable. An executable needs to have a root module + // which needs to expose a `main` function. While we could add a main function + // to the module defined above, it's sometimes preferable to split business + // business logic and the CLI into two separate modules. + // + // If your goal is to create a Zig library for others to use, consider if + // it might benefit from also exposing a CLI tool. A parser library for a + // data serialization format could also bundle a CLI syntax checker, for example. + // + // If instead your goal is to create an executable, consider if users might + // be interested in also being able to embed the core functionality of your + // program in their own executable in order to avoid the overhead involved in + // subprocessing your CLI tool. + // + // If neither case applies to you, feel free to delete the declaration you + // don't need and to put everything under a single module. + const exe = b.addExecutable(.{ + .name = "code_index_example", + .root_module = b.createModule(.{ + // b.createModule defines a new module just like b.addModule but, + // unlike b.addModule, it does not expose the module to consumers of + // this package, which is why in this case we don't have to give it a name. + .root_source_file = b.path("src/main.zig"), + // Target and optimization levels must be explicitly wired in when + // defining an executable or library (in the root module), and you + // can also hardcode a specific target for an executable or library + // definition if desireable (e.g. firmware for embedded devices). + .target = target, + .optimize = optimize, + // List of modules available for import in source files part of the + // root module. + .imports = &.{ + // Here "code_index_example" is the name you will use in your source code to + // import this module (e.g. `@import("code_index_example")`). The name is + // repeated because you are allowed to rename your imports, which + // can be extremely useful in case of collisions (which can happen + // importing modules from different packages). + .{ .name = "code_index_example", .module = mod }, + }, + }), + }); + + // This declares intent for the executable to be installed into the + // install prefix when running `zig build` (i.e. when executing the default + // step). By default the install prefix is `zig-out/` but can be overridden + // by passing `--prefix` or `-p`. + b.installArtifact(exe); + + // This creates a top level step. Top level steps have a name and can be + // invoked by name when running `zig build` (e.g. `zig build run`). + // This will evaluate the `run` step rather than the default step. + // For a top level step to actually do something, it must depend on other + // steps (e.g. a Run step, as we will see in a moment). + const run_step = b.step("run", "Run the app"); + + // This creates a RunArtifact step in the build graph. A RunArtifact step + // invokes an executable compiled by Zig. Steps will only be executed by the + // runner if invoked directly by the user (in the case of top level steps) + // or if another step depends on it, so it's up to you to define when and + // how this Run step will be executed. In our case we want to run it when + // the user runs `zig build run`, so we create a dependency link. + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + // By making the run step depend on the default step, it will be run from the + // installation directory rather than directly from within the cache directory. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // Creates an executable that will run `test` blocks from the provided module. + // Here `mod` needs to define a target, which is why earlier we made sure to + // set the releative field. + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + + // A run step that will run the test executable. + const run_mod_tests = b.addRunArtifact(mod_tests); + + // Creates an executable that will run `test` blocks from the executable's + // root module. Note that test executables only test one module at a time, + // hence why we have to create two separate ones. + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, + }); + + // A run step that will run the second test executable. + const run_exe_tests = b.addRunArtifact(exe_tests); + + // A top level step for running all tests. dependOn can be called multiple + // times and since the two run steps do not depend on one another, this will + // make the two of them run in parallel. + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); + test_step.dependOn(&run_exe_tests.step); + + // Just like flags, top level steps are also listed in the `--help` menu. + // + // The Zig build system is entirely implemented in userland, which means + // that it cannot hook into private compiler APIs. All compilation work + // orchestrated by the build system will result in other Zig compiler + // subcommands being invoked with the right flags defined. You can observe + // these invocations when one fails (or you pass a flag to increase + // verbosity) to validate assumptions and diagnose problems. + // + // Lastly, the Zig build system is relatively simple and self-contained, + // and reading its source code will allow you to master it. +} diff --git a/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/build.zig.zon b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/build.zig.zon new file mode 100644 index 00000000..f8f4ad10 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/build.zig.zon @@ -0,0 +1,81 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .code_index_example, + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0x995c7acfb423849b, // Changing this has security and trust implications. + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.15.0-dev.1507+e25168d01", + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/main.zig b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/main.zig new file mode 100644 index 00000000..792cfc13 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/main.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const testing = @import("testing"); +const code_index_example = @import("code_index_example"); +const utils = @import("./utils.zig"); +const math_utils = @import("./math.zig"); + +pub fn main() !void { + // Prints to stderr, ignoring potential errors. + std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + try code_index_example.bufferedPrint(); + + // Test our custom utilities + const result = utils.processData("Hello, World!"); + std.debug.print("Processed result: {s}\n", .{result}); + + // Test math utilities + const sum = math_utils.calculateSum(10, 20); + std.debug.print("Sum: {}\n", .{sum}); + + // Platform-specific code + if (builtin.os.tag == .windows) { + std.debug.print("Running on Windows\n", .{}); + } else { + std.debug.print("Running on Unix-like system\n", .{}); + } +} + +test "simple test" { + var list = std.ArrayList(i32).init(std.testing.allocator); + defer list.deinit(); // Try commenting this out and see if zig detects the memory leak! + try list.append(42); + try std.testing.expectEqual(@as(i32, 42), list.pop()); +} + +test "fuzz example" { + const Context = struct { + fn testOne(context: @This(), input: []const u8) anyerror!void { + _ = context; + // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case! + try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input)); + } + }; + try std.testing.fuzz(Context{}, Context.testOne, .{}); +} diff --git a/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/math.zig b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/math.zig new file mode 100644 index 00000000..dba74205 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/math.zig @@ -0,0 +1,262 @@ +//! Mathematical utility functions and data structures +const std = @import("std"); +const math = @import("math"); +const testing = @import("testing"); + +// Mathematical constants +pub const PI: f64 = 3.14159265358979323846; +pub const E: f64 = 2.71828182845904523536; +pub const GOLDEN_RATIO: f64 = 1.61803398874989484820; + +// Complex number representation +pub const Complex = struct { + real: f64, + imag: f64, + + pub fn init(real: f64, imag: f64) Complex { + return Complex{ .real = real, .imag = imag }; + } + + pub fn add(self: Complex, other: Complex) Complex { + return Complex{ + .real = self.real + other.real, + .imag = self.imag + other.imag, + }; + } + + pub fn multiply(self: Complex, other: Complex) Complex { + return Complex{ + .real = self.real * other.real - self.imag * other.imag, + .imag = self.real * other.imag + self.imag * other.real, + }; + } + + pub fn magnitude(self: Complex) f64 { + return @sqrt(self.real * self.real + self.imag * self.imag); + } + + pub fn conjugate(self: Complex) Complex { + return Complex{ .real = self.real, .imag = -self.imag }; + } +}; + +// Point in 2D space +pub const Point2D = struct { + x: f64, + y: f64, + + pub fn init(x: f64, y: f64) Point2D { + return Point2D{ .x = x, .y = y }; + } + + pub fn distance(self: Point2D, other: Point2D) f64 { + const dx = self.x - other.x; + const dy = self.y - other.y; + return @sqrt(dx * dx + dy * dy); + } + + pub fn midpoint(self: Point2D, other: Point2D) Point2D { + return Point2D{ + .x = (self.x + other.x) / 2.0, + .y = (self.y + other.y) / 2.0, + }; + } +}; + +// Statistics utilities +pub const Statistics = struct { + pub fn mean(values: []const f64) f64 { + if (values.len == 0) return 0.0; + + var sum: f64 = 0.0; + for (values) |value| { + sum += value; + } + + return sum / @as(f64, @floatFromInt(values.len)); + } + + pub fn median(values: []const f64, buffer: []f64) f64 { + if (values.len == 0) return 0.0; + + // Copy to buffer and sort + for (values, 0..) |value, i| { + buffer[i] = value; + } + std.sort.insertionSort(f64, buffer[0..values.len], {}, std.sort.asc(f64)); + + const n = values.len; + if (n % 2 == 1) { + return buffer[n / 2]; + } else { + return (buffer[n / 2 - 1] + buffer[n / 2]) / 2.0; + } + } + + pub fn standardDeviation(values: []const f64) f64 { + if (values.len <= 1) return 0.0; + + const avg = mean(values); + var sum_sq_diff: f64 = 0.0; + + for (values) |value| { + const diff = value - avg; + sum_sq_diff += diff * diff; + } + + return @sqrt(sum_sq_diff / @as(f64, @floatFromInt(values.len - 1))); + } +}; + +// Basic math functions +pub fn factorial(n: u32) u64 { + if (n <= 1) return 1; + return @as(u64, n) * factorial(n - 1); +} + +pub fn fibonacci(n: u32) u64 { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} + +pub fn gcd(a: u32, b: u32) u32 { + if (b == 0) return a; + return gcd(b, a % b); +} + +pub fn lcm(a: u32, b: u32) u32 { + return (a * b) / gcd(a, b); +} + +pub fn isPrime(n: u32) bool { + if (n < 2) return false; + if (n == 2) return true; + if (n % 2 == 0) return false; + + var i: u32 = 3; + while (i * i <= n) : (i += 2) { + if (n % i == 0) return false; + } + + return true; +} + +// Function used by main.zig +pub fn calculateSum(a: i32, b: i32) i32 { + return a + b; +} + +pub fn power(base: f64, exponent: i32) f64 { + if (exponent == 0) return 1.0; + if (exponent < 0) return 1.0 / power(base, -exponent); + + var result: f64 = 1.0; + var exp = exponent; + var b = base; + + while (exp > 0) { + if (exp % 2 == 1) { + result *= b; + } + b *= b; + exp /= 2; + } + + return result; +} + +// Matrix operations (2x2 for simplicity) +pub const Matrix2x2 = struct { + data: [2][2]f64, + + pub fn init(a: f64, b: f64, c: f64, d: f64) Matrix2x2 { + return Matrix2x2{ + .data = [_][2]f64{ + [_]f64{ a, b }, + [_]f64{ c, d }, + }, + }; + } + + pub fn multiply(self: Matrix2x2, other: Matrix2x2) Matrix2x2 { + return Matrix2x2{ + .data = [_][2]f64{ + [_]f64{ + self.data[0][0] * other.data[0][0] + self.data[0][1] * other.data[1][0], + self.data[0][0] * other.data[0][1] + self.data[0][1] * other.data[1][1], + }, + [_]f64{ + self.data[1][0] * other.data[0][0] + self.data[1][1] * other.data[1][0], + self.data[1][0] * other.data[0][1] + self.data[1][1] * other.data[1][1], + }, + }, + }; + } + + pub fn determinant(self: Matrix2x2) f64 { + return self.data[0][0] * self.data[1][1] - self.data[0][1] * self.data[1][0]; + } +}; + +// Tests +test "complex number operations" { + const z1 = Complex.init(3.0, 4.0); + const z2 = Complex.init(1.0, 2.0); + + const sum = z1.add(z2); + try std.testing.expectEqual(@as(f64, 4.0), sum.real); + try std.testing.expectEqual(@as(f64, 6.0), sum.imag); + + const magnitude = z1.magnitude(); + try std.testing.expectApproxEqAbs(@as(f64, 5.0), magnitude, 0.0001); +} + +test "point distance calculation" { + const p1 = Point2D.init(0.0, 0.0); + const p2 = Point2D.init(3.0, 4.0); + + const dist = p1.distance(p2); + try std.testing.expectApproxEqAbs(@as(f64, 5.0), dist, 0.0001); +} + +test "factorial calculation" { + try std.testing.expectEqual(@as(u64, 1), factorial(0)); + try std.testing.expectEqual(@as(u64, 1), factorial(1)); + try std.testing.expectEqual(@as(u64, 120), factorial(5)); +} + +test "fibonacci sequence" { + try std.testing.expectEqual(@as(u64, 0), fibonacci(0)); + try std.testing.expectEqual(@as(u64, 1), fibonacci(1)); + try std.testing.expectEqual(@as(u64, 13), fibonacci(7)); +} + +test "prime number detection" { + try std.testing.expect(isPrime(2)); + try std.testing.expect(isPrime(17)); + try std.testing.expect(!isPrime(4)); + try std.testing.expect(!isPrime(1)); +} + +test "statistics calculations" { + const values = [_]f64{ 1.0, 2.0, 3.0, 4.0, 5.0 }; + + const avg = Statistics.mean(&values); + try std.testing.expectEqual(@as(f64, 3.0), avg); + + var buffer: [10]f64 = undefined; + const med = Statistics.median(&values, &buffer); + try std.testing.expectEqual(@as(f64, 3.0), med); +} + +test "matrix operations" { + const m1 = Matrix2x2.init(1.0, 2.0, 3.0, 4.0); + const m2 = Matrix2x2.init(5.0, 6.0, 7.0, 8.0); + + const product = m1.multiply(m2); + try std.testing.expectEqual(@as(f64, 19.0), product.data[0][0]); + try std.testing.expectEqual(@as(f64, 22.0), product.data[0][1]); + + const det = m1.determinant(); + try std.testing.expectEqual(@as(f64, -2.0), det); +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/root.zig b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/root.zig new file mode 100644 index 00000000..1cc95e33 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/root.zig @@ -0,0 +1,135 @@ +//! By convention, root.zig is the root source file when making a library. +const std = @import("std"); +const fmt = @import("fmt"); +const mem = @import("mem"); +const json = @import("json"); + +// Define custom types and structures +pub const Config = struct { + name: []const u8, + version: u32, + debug: bool, + + pub fn init(name: []const u8, version: u32) Config { + return Config{ + .name = name, + .version = version, + .debug = false, + }; + } + + pub fn setDebug(self: *Config, debug: bool) void { + self.debug = debug; + } +}; + +pub const ErrorType = enum { + None, + InvalidInput, + OutOfMemory, + NetworkError, + + pub fn toString(self: ErrorType) []const u8 { + return switch (self) { + .None => "No error", + .InvalidInput => "Invalid input", + .OutOfMemory => "Out of memory", + .NetworkError => "Network error", + }; + } +}; + +// Global constants +pub const VERSION: u32 = 1; +pub const MAX_BUFFER_SIZE: usize = 4096; +var global_config: Config = undefined; + +pub fn bufferedPrint() !void { + // Stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + var stdout_buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + const stdout = &stdout_writer.interface; + + try stdout.print("Run `zig build test` to run the tests.\n", .{}); + + try stdout.flush(); // Don't forget to flush! +} + +pub fn add(a: i32, b: i32) i32 { + return a + b; +} + +pub fn multiply(a: i32, b: i32) i32 { + return a * b; +} + +pub fn processConfig(config: *const Config) !void { + std.debug.print("Processing config: {s} v{}\n", .{ config.name, config.version }); + if (config.debug) { + std.debug.print("Debug mode enabled\n", .{}); + } +} + +pub fn handleError(err: ErrorType) void { + std.debug.print("Error: {s}\n", .{err.toString()}); +} + +// Advanced function with error handling +pub fn parseNumber(input: []const u8) !i32 { + if (input.len == 0) { + return error.InvalidInput; + } + + return std.fmt.parseInt(i32, input, 10) catch |err| switch (err) { + error.InvalidCharacter => error.InvalidInput, + error.Overflow => error.OutOfMemory, + else => err, + }; +} + +// Generic function +pub fn swap(comptime T: type, a: *T, b: *T) void { + const temp = a.*; + a.* = b.*; + b.* = temp; +} + +test "basic add functionality" { + try std.testing.expect(add(3, 7) == 10); +} + +test "config initialization" { + var config = Config.init("test-app", 1); + try std.testing.expectEqualStrings("test-app", config.name); + try std.testing.expectEqual(@as(u32, 1), config.version); + try std.testing.expectEqual(false, config.debug); + + config.setDebug(true); + try std.testing.expectEqual(true, config.debug); +} + +test "error type handling" { + const err = ErrorType.InvalidInput; + try std.testing.expectEqualStrings("Invalid input", err.toString()); +} + +test "number parsing" { + const result = try parseNumber("42"); + try std.testing.expectEqual(@as(i32, 42), result); + + // Test error case + const invalid_result = parseNumber(""); + try std.testing.expectError(error.InvalidInput, invalid_result); +} + +test "generic swap function" { + var a: i32 = 10; + var b: i32 = 20; + + swap(i32, &a, &b); + + try std.testing.expectEqual(@as(i32, 20), a); + try std.testing.expectEqual(@as(i32, 10), b); +} diff --git a/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/utils.zig b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/utils.zig new file mode 100644 index 00000000..eab54ce8 --- /dev/null +++ b/reference/code-index-mcp-master/test/sample-projects/zig/code-index-example/src/utils.zig @@ -0,0 +1,169 @@ +//! Utility functions for string processing and data manipulation +const std = @import("std"); +const mem = @import("mem"); +const ascii = @import("ascii"); + +// Constants for utility functions +pub const DEFAULT_BUFFER_SIZE: usize = 256; +pub const MAX_STRING_LENGTH: usize = 1024; + +// Custom error types +pub const UtilError = error{ + BufferTooSmall, + InvalidString, + ProcessingFailed, +}; + +// String processing utilities +pub const StringProcessor = struct { + buffer: []u8, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator, buffer_size: usize) !StringProcessor { + const buffer = try allocator.alloc(u8, buffer_size); + return StringProcessor{ + .buffer = buffer, + .allocator = allocator, + }; + } + + pub fn deinit(self: *StringProcessor) void { + self.allocator.free(self.buffer); + } + + pub fn toUpperCase(self: *StringProcessor, input: []const u8) ![]const u8 { + if (input.len > self.buffer.len) { + return UtilError.BufferTooSmall; + } + + for (input, 0..) |char, i| { + self.buffer[i] = std.ascii.toUpper(char); + } + + return self.buffer[0..input.len]; + } + + pub fn reverse(self: *StringProcessor, input: []const u8) ![]const u8 { + if (input.len > self.buffer.len) { + return UtilError.BufferTooSmall; + } + + for (input, 0..) |char, i| { + self.buffer[input.len - 1 - i] = char; + } + + return self.buffer[0..input.len]; + } +}; + +// Data validation functions +pub fn validateEmail(email: []const u8) bool { + if (email.len == 0) return false; + + var has_at = false; + var has_dot = false; + + for (email) |char| { + if (char == '@') { + if (has_at) return false; // Multiple @ symbols + has_at = true; + } else if (char == '.') { + has_dot = true; + } + } + + return has_at and has_dot; +} + +pub fn isValidIdentifier(identifier: []const u8) bool { + if (identifier.len == 0) return false; + + // First character must be letter or underscore + if (!std.ascii.isAlphabetic(identifier[0]) and identifier[0] != '_') { + return false; + } + + // Rest must be alphanumeric or underscore + for (identifier[1..]) |char| { + if (!std.ascii.isAlphanumeric(char) and char != '_') { + return false; + } + } + + return true; +} + +// Simple string processing function used by main.zig +pub fn processData(input: []const u8) []const u8 { + return if (input.len > 0) "Processed!" else "Empty input"; +} + +// Array utilities +pub fn findMax(numbers: []const i32) ?i32 { + if (numbers.len == 0) return null; + + var max = numbers[0]; + for (numbers[1..]) |num| { + if (num > max) { + max = num; + } + } + + return max; +} + +pub fn bubbleSort(numbers: []i32) void { + const n = numbers.len; + if (n <= 1) return; + + var i: usize = 0; + while (i < n - 1) : (i += 1) { + var j: usize = 0; + while (j < n - i - 1) : (j += 1) { + if (numbers[j] > numbers[j + 1]) { + const temp = numbers[j]; + numbers[j] = numbers[j + 1]; + numbers[j + 1] = temp; + } + } + } +} + +// Tests +test "string processor initialization" { + var processor = try StringProcessor.init(std.testing.allocator, 100); + defer processor.deinit(); + + const result = try processor.toUpperCase("hello"); + try std.testing.expectEqualStrings("HELLO", result); +} + +test "email validation" { + try std.testing.expect(validateEmail("test@example.com")); + try std.testing.expect(!validateEmail("invalid-email")); + try std.testing.expect(!validateEmail("")); +} + +test "identifier validation" { + try std.testing.expect(isValidIdentifier("valid_id")); + try std.testing.expect(isValidIdentifier("_private")); + try std.testing.expect(!isValidIdentifier("123invalid")); + try std.testing.expect(!isValidIdentifier("")); +} + +test "find maximum in array" { + const numbers = [_]i32{ 3, 1, 4, 1, 5, 9, 2, 6 }; + const max = findMax(&numbers); + try std.testing.expectEqual(@as(?i32, 9), max); + + const empty: []const i32 = &[_]i32{}; + try std.testing.expectEqual(@as(?i32, null), findMax(empty)); +} + +test "bubble sort" { + var numbers = [_]i32{ 64, 34, 25, 12, 22, 11, 90 }; + bubbleSort(&numbers); + + const expected = [_]i32{ 11, 12, 22, 25, 34, 64, 90 }; + try std.testing.expectEqualSlices(i32, &expected, &numbers); +} \ No newline at end of file diff --git a/reference/code-index-mcp-master/tests/indexing/test_javascript_called_by.py b/reference/code-index-mcp-master/tests/indexing/test_javascript_called_by.py new file mode 100644 index 00000000..66bd9115 --- /dev/null +++ b/reference/code-index-mcp-master/tests/indexing/test_javascript_called_by.py @@ -0,0 +1,82 @@ +from pathlib import Path + +from code_index_mcp.indexing.sqlite_index_manager import SQLiteIndexManager + + +def test_javascript_user_service_called_by(): + sample_project = ( + Path(__file__).resolve().parents[2] + / "test" + / "sample-projects" + / "javascript" + / "user-management" + ) + assert sample_project.exists() + + manager = SQLiteIndexManager() + assert manager.set_project_path(str(sample_project)) + assert manager.build_index() + assert manager.load_index() + + summary = manager.get_file_summary("src/services/UserService.js") + assert summary is not None + + method_calls = {info["name"]: info["called_by"] for info in summary["methods"]} + + expected_calls = { + "UserService.createUser": ["src/routes/userRoutes.js:106"], + "UserService.authenticateUser": ["src/routes/userRoutes.js:116"], + "UserService.getAllUsers": ["src/routes/userRoutes.js:139"], + "UserService.searchUsers": ["src/routes/userRoutes.js:148"], + "UserService.getUserStats": ["src/routes/userRoutes.js:156"], + "UserService.exportUsers": ["src/routes/userRoutes.js:164"], + "UserService.getActiveUsers": ["src/routes/userRoutes.js:172"], + "UserService.getUsersByRole": ["src/routes/userRoutes.js:181"], + "UserService.getUserById": ["src/routes/userRoutes.js:189"], + "UserService.getUserActivity": ["src/routes/userRoutes.js:197"], + "UserService.updateUser": ["src/routes/userRoutes.js:205"], + "UserService.changePassword": ["src/routes/userRoutes.js:215"], + "UserService.resetPassword": ["src/routes/userRoutes.js:225"], + "UserService.addPermission": ["src/routes/userRoutes.js:235"], + "UserService.removePermission": ["src/routes/userRoutes.js:245"], + "UserService.deleteUser": ["src/routes/userRoutes.js:254"], + "UserService.hardDeleteUser": ["src/routes/userRoutes.js:263"], + } + + for method_name, callers in expected_calls.items(): + assert method_calls.get(method_name) == callers + + assert method_calls.get("UserService.getUserByUsername") == [] + assert method_calls.get("UserService.getUserByEmail") == [] + + auth_summary = manager.get_file_summary("src/middleware/auth.js") + assert auth_summary is not None + + auth_functions = {info["name"]: info["called_by"] for info in auth_summary["functions"]} + + expected_auth_callers = [ + "src/routes/userRoutes.js:124", + "src/routes/userRoutes.js:146", + "src/routes/userRoutes.js:155", + "src/routes/userRoutes.js:163", + "src/routes/userRoutes.js:171", + "src/routes/userRoutes.js:179", + "src/routes/userRoutes.js:188", + "src/routes/userRoutes.js:196", + "src/routes/userRoutes.js:204", + "src/routes/userRoutes.js:213", + "src/routes/userRoutes.js:223", + "src/routes/userRoutes.js:233", + "src/routes/userRoutes.js:243", + "src/routes/userRoutes.js:253", + "src/routes/userRoutes.js:262", + ] + + assert sorted(auth_functions.get("auth", [])) == expected_auth_callers + assert auth_functions.get("authorize") == [ + "src/middleware/auth.js:120", + "src/middleware/auth.js:126", + ] + assert auth_functions.get("requirePermission") == [] + assert auth_functions.get("selfOrAdmin") == [] + assert auth_functions.get("optionalAuth") == [] diff --git a/reference/code-index-mcp-master/tests/indexing/test_shallow_index_lenient.py b/reference/code-index-mcp-master/tests/indexing/test_shallow_index_lenient.py new file mode 100644 index 00000000..9cb4f797 --- /dev/null +++ b/reference/code-index-mcp-master/tests/indexing/test_shallow_index_lenient.py @@ -0,0 +1,41 @@ +from pathlib import Path + +import pytest + +from code_index_mcp.indexing.shallow_index_manager import ShallowIndexManager + + +@pytest.fixture() +def temp_manager(tmp_path): + project = Path("test/sample-projects/python/user_management").resolve() + m = ShallowIndexManager() + assert m.set_project_path(str(project)) + assert m.build_index() + assert m.load_index() + yield m + m.cleanup() + + +def test_simple_filename_triggers_recursive(temp_manager): + res = temp_manager.find_files("user.py") + assert "models/user.py" in res + + +def test_case_insensitive(temp_manager): + res = temp_manager.find_files("USER.PY") + assert "models/user.py" in [p.lower() for p in res] + + +def test_pattern_with_slash_not_lenient(temp_manager): + res = temp_manager.find_files("models/user.py") + assert res == ["models/user.py"] + + +def test_wildcard_all_unchanged(temp_manager): + res = temp_manager.find_files("*") + # sample project has 12 files + assert len(res) == 12 + + +def test_non_string_returns_empty(temp_manager): + assert temp_manager.find_files(None) == [] # type: ignore[arg-type] diff --git a/reference/code-index-mcp-master/tests/indexing/test_sqlite_index_manager.py b/reference/code-index-mcp-master/tests/indexing/test_sqlite_index_manager.py new file mode 100644 index 00000000..f2626ef0 --- /dev/null +++ b/reference/code-index-mcp-master/tests/indexing/test_sqlite_index_manager.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from code_index_mcp.indexing.sqlite_index_manager import SQLiteIndexManager + + +def test_sqlite_index_manager_builds_and_queries(tmp_path): + sample_project = ( + Path(__file__).resolve().parents[2] + / "test" + / "sample-projects" + / "python" + / "user_management" + ) + assert sample_project.exists() + + manager = SQLiteIndexManager() + assert manager.set_project_path(str(sample_project)) + + assert manager.build_index() + assert manager.load_index() + + summary = manager.get_file_summary("services/user_manager.py") + assert summary is not None + assert summary["file_path"] == "services/user_manager.py" + assert summary["language"].lower() == "python" + assert summary["symbol_count"] > 0 + + stats = manager.get_index_stats() + assert stats["status"] == "loaded" + assert stats["indexed_files"] > 0 diff --git a/reference/code-index-mcp-master/tests/indexing/test_sqlite_store.py b/reference/code-index-mcp-master/tests/indexing/test_sqlite_store.py new file mode 100644 index 00000000..144c342a --- /dev/null +++ b/reference/code-index-mcp-master/tests/indexing/test_sqlite_store.py @@ -0,0 +1,55 @@ +from code_index_mcp.indexing.sqlite_store import ( + SCHEMA_VERSION, + SQLiteIndexStore, + SQLiteSchemaMismatchError, +) + + +def test_initialize_schema_creates_tables(tmp_path): + db_path = tmp_path / "index.db" + store = SQLiteIndexStore(str(db_path)) + + store.initialize_schema() + + assert db_path.exists() + with store.connect() as conn: + tables = { + row["name"] + for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + } + assert {"metadata", "files", "symbols"} <= tables + schema_version = store.get_metadata(conn, "schema_version") + assert schema_version == SCHEMA_VERSION + + +def test_schema_mismatch_raises(tmp_path): + db_path = tmp_path / "index.db" + store = SQLiteIndexStore(str(db_path)) + store.initialize_schema() + + # Manually tamper schema version + with store.connect() as conn: + conn.execute( + "UPDATE metadata SET value = ? WHERE key = 'schema_version'", + ("0",), + ) + + try: + store.initialize_schema() + except SQLiteSchemaMismatchError: + pass + else: + raise AssertionError("Expected schema mismatch to raise error") + + +def test_set_and_get_metadata_roundtrip(tmp_path): + db_path = tmp_path / "index.db" + store = SQLiteIndexStore(str(db_path)) + store.initialize_schema() + + with store.connect() as conn: + store.set_metadata(conn, "project_path", "/tmp/test-project") + conn.commit() + + with store.connect() as conn: + assert store.get_metadata(conn, "project_path") == "/tmp/test-project" diff --git a/reference/code-index-mcp-master/tests/indexing/test_symbol_ids.py b/reference/code-index-mcp-master/tests/indexing/test_symbol_ids.py new file mode 100644 index 00000000..7fd89574 --- /dev/null +++ b/reference/code-index-mcp-master/tests/indexing/test_symbol_ids.py @@ -0,0 +1,30 @@ +"""Tests for symbol identifier generation.""" + +from code_index_mcp.indexing.json_index_builder import JSONIndexBuilder + + +def test_symbol_ids_use_relative_paths(tmp_path): + project_dir = tmp_path / "project" + scripts_dir = project_dir / "scripts" + examples_dir = project_dir / "examples" + scripts_dir.mkdir(parents=True) + examples_dir.mkdir(parents=True) + + (scripts_dir / "foo.py").write_text( + "def foo():\n" + " return 1\n", + encoding="utf-8", + ) + (examples_dir / "foo.py").write_text( + "def foo():\n" + " return 2\n", + encoding="utf-8", + ) + + builder = JSONIndexBuilder(str(project_dir)) + index = builder.build_index(parallel=False) + symbols = index["symbols"] + + assert "scripts/foo.py::foo" in symbols + assert "examples/foo.py::foo" in symbols + assert len({sid for sid in symbols if sid.endswith("::foo")}) == 2 diff --git a/reference/code-index-mcp-master/tests/indexing/test_typescript_called_by.py b/reference/code-index-mcp-master/tests/indexing/test_typescript_called_by.py new file mode 100644 index 00000000..20aab425 --- /dev/null +++ b/reference/code-index-mcp-master/tests/indexing/test_typescript_called_by.py @@ -0,0 +1,38 @@ +from pathlib import Path + +from code_index_mcp.indexing.sqlite_index_manager import SQLiteIndexManager + + +def test_typescript_rate_limiter_called_by(): + sample_project = ( + Path(__file__).resolve().parents[2] + / "test" + / "sample-projects" + / "typescript" + / "user-management" + ) + assert sample_project.exists() + + manager = SQLiteIndexManager() + assert manager.set_project_path(str(sample_project)) + assert manager.build_index() + assert manager.load_index() + + summary = manager.get_file_summary("src/middleware/rateLimiter.ts") + assert summary is not None + + limiter_calls = {info["name"]: info["called_by"] for info in summary["functions"]} + + expected_calls = { + "generalLimiter": ["src/server.ts:59"], + "createUserLimiter": ["src/routes/userRoutes.ts:121"], + "authLimiter": ["src/routes/userRoutes.ts:136"], + "exportLimiter": ["src/routes/userRoutes.ts:209"], + } + + for limiter, callers in expected_calls.items(): + assert limiter_calls.get(limiter) == callers + + assert limiter_calls.get("passwordResetLimiter") == [] + assert limiter_calls.get("docsLimiter") == [] + assert limiter_calls.get("createCustomLimiter") == [] diff --git a/reference/code-index-mcp-master/tests/search/test_search_filters.py b/reference/code-index-mcp-master/tests/search/test_search_filters.py new file mode 100644 index 00000000..787461d4 --- /dev/null +++ b/reference/code-index-mcp-master/tests/search/test_search_filters.py @@ -0,0 +1,52 @@ +"""Tests covering shared search filtering behaviour.""" +import os +from types import SimpleNamespace +from unittest.mock import patch +from pathlib import Path as _TestPath +import sys + +ROOT = _TestPath(__file__).resolve().parents[2] +SRC_PATH = ROOT / 'src' +if str(SRC_PATH) not in sys.path: + sys.path.insert(0, str(SRC_PATH)) + +from code_index_mcp.search.basic import BasicSearchStrategy +from code_index_mcp.search.ripgrep import RipgrepStrategy +from code_index_mcp.utils.file_filter import FileFilter + + +def test_basic_strategy_skips_excluded_directories(tmp_path): + base = tmp_path + src_dir = base / "src" + src_dir.mkdir() + (src_dir / 'app.js').write_text("const db = 'mongo';\n") + + node_modules_dir = base / "node_modules" / "pkg" + node_modules_dir.mkdir(parents=True) + (node_modules_dir / 'index.js').write_text("// mongo dependency\n") + + strategy = BasicSearchStrategy() + strategy.configure_excludes(FileFilter()) + + results = strategy.search("mongo", str(base), case_sensitive=False) + + included_path = os.path.join("src", "app.js") + excluded_path = os.path.join("node_modules", "pkg", "index.js") + + assert included_path in results + assert excluded_path not in results + + +@patch("code_index_mcp.search.ripgrep.subprocess.run") +def test_ripgrep_strategy_adds_exclude_globs(mock_run, tmp_path): + mock_run.return_value = SimpleNamespace(returncode=0, stdout="", stderr="") + + strategy = RipgrepStrategy() + strategy.configure_excludes(FileFilter()) + + strategy.search("mongo", str(tmp_path)) + + cmd = mock_run.call_args[0][0] + glob_args = [cmd[i + 1] for i, arg in enumerate(cmd) if arg == '--glob' and i + 1 < len(cmd)] + + assert any(value.startswith('!**/node_modules/') for value in glob_args) diff --git a/reference/code-index-mcp-master/tests/search/test_search_service_pagination.py b/reference/code-index-mcp-master/tests/search/test_search_service_pagination.py new file mode 100644 index 00000000..186ed8d8 --- /dev/null +++ b/reference/code-index-mcp-master/tests/search/test_search_service_pagination.py @@ -0,0 +1,98 @@ +"""Tests for search result pagination formatting.""" +from pathlib import Path as _TestPath +from types import SimpleNamespace +import sys + +ROOT = _TestPath(__file__).resolve().parents[2] +SRC_PATH = ROOT / 'src' +if str(SRC_PATH) not in sys.path: + sys.path.insert(0, str(SRC_PATH)) + +from code_index_mcp.services.search_service import SearchService + + +def _create_service() -> SearchService: + ctx = SimpleNamespace( + request_context=SimpleNamespace( + lifespan_context=SimpleNamespace(base_path="", settings=None) + ) + ) + return SearchService(ctx) + + +def test_paginate_results_default_ordering(): + service = _create_service() + + raw_results = { + "b/file.py": [(12, "second match"), (3, "first match")], + "a/file.py": [(8, "another file")], + } + + formatted, pagination = service._paginate_results( + raw_results, + start_index=0, + max_results=None, + ) + + assert pagination == { + "total_matches": 3, + "returned": 3, + "start_index": 0, + "has_more": False, + "end_index": 3, + } + + assert formatted == [ + {"file": "a/file.py", "line": 8, "text": "another file"}, + {"file": "b/file.py", "line": 3, "text": "first match"}, + {"file": "b/file.py", "line": 12, "text": "second match"}, + ] + + +def test_paginate_results_with_start_and_limit(): + service = _create_service() + + raw_results = { + "b/file.py": [(5, "line five"), (6, "line six")], + "a/file.py": [(1, "line one"), (2, "line two")], + } + + formatted, pagination = service._paginate_results( + raw_results, + start_index=1, + max_results=2, + ) + + assert pagination == { + "total_matches": 4, + "returned": 2, + "start_index": 1, + "has_more": True, + "max_results": 2, + "end_index": 3, + } + + assert formatted == [ + {"file": "a/file.py", "line": 2, "text": "line two"}, + {"file": "b/file.py", "line": 5, "text": "line five"}, + ] + + +def test_paginate_results_when_start_beyond_total(): + service = _create_service() + + formatted, pagination = service._paginate_results( + {"only/file.py": [(1, "match")]}, + start_index=10, + max_results=5, + ) + + assert formatted == [] + assert pagination == { + "total_matches": 1, + "returned": 0, + "start_index": 1, + "has_more": False, + "max_results": 5, + "end_index": 1, + } diff --git a/reference/code-index-mcp-master/tests/strategies/test_go_discovery.py b/reference/code-index-mcp-master/tests/strategies/test_go_discovery.py new file mode 100644 index 00000000..b7fe1132 --- /dev/null +++ b/reference/code-index-mcp-master/tests/strategies/test_go_discovery.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Test for Go symbol discovery including all symbol types.""" +import pytest + +from code_index_mcp.indexing.strategies.go_strategy import GoParsingStrategy + + +@pytest.fixture +def test_code_with_all_symbols(): + """Fixture with variables, constants, functions, structs, interfaces and methods.""" + return ''' +package main + +import ( + "fmt" // comment mentioning ) parentheses ) + strutil "strings" + helper "example.com/project/toolkit" +) // closing comment with ) ) characters +import ("math" /* inline comment ) inside */) +import . "errors" +import _ "embed" +import ( + "time" +) + +// Application version constant +const VERSION = "1.0.0" + +// Global variable for configuration +var config string = "default" + +// Add returns the sum of two integers. +func Add(x int, y int) int { + return x + y +} + +// ComplexFunc demonstrates a complex function signature. +// It takes multiple parameters including variadic args. +func ComplexFunc(name string, values ...int) (int, error) { + sum := 0 + for _, v := range values { + sum += v + } + return sum, nil +} + +// Divide divides two numbers and returns the result. +// It returns an error if the divisor is zero. +// +// Parameters: +// - a: the dividend +// - b: the divisor +// +// Returns: +// - float64: the quotient +// - error: an error if b is zero +func Divide(a, b float64) (float64, error) { + if b == 0 { + return 0, errors.New("division by zero") + } + return a / b, nil +} + +// ProcessData processes the given data with options. +// +// The function accepts a data string and processes it according to +// the provided options. It supports multiple processing modes. +// +// Args: +// data: the input data string to process +// options: configuration options for processing +// +// Returns: +// The processed result string and any error encountered. +// +// Example: +// result, err := ProcessData("hello", Options{Mode: "upper"}) +// if err != nil { +// log.Fatal(err) +// } +func ProcessData(data string, options map[string]interface{}) (string, error) { + // Implementation + return strings.ToUpper(data), nil +} + +// User represents a user in the system. +type User struct { + ID int + Name string + Email string + IsActive bool +} + +// NewUser creates a new User instance. +func NewUser(id int, name string, email string) *User { + return &User{ + ID: id, + Name: name, + Email: email, + } +} + +// GetFullName returns the user's full name. +func (u *User) GetFullName() string { + return u.Name +} + +// Activate marks the user as active. +// This is a pointer receiver method. +func (u *User) Activate() { + u.IsActive = true +} + +// IsValid checks if the user has valid data. +// This is a value receiver method. +func (u User) IsValid() bool { + return u.ID > 0 && u.Name != "" +} + +// Repository interface defines data access methods. +type Repository interface { + // Save stores an entity + Save(entity interface{}) error + + // FindByID retrieves an entity by ID + FindByID(id int) (interface{}, error) + + // Delete removes an entity + Delete(id int) error +} + +// Logger is a simple interface for logging. +type Logger interface { + Log(message string) +} + +// Calculate is a function type +type Calculate func(a, b int) int +''' + + +def test_go_symbol_discovery(test_code_with_all_symbols): + """Test that all Go symbol types are correctly discovered.""" + strategy = GoParsingStrategy() + symbols, file_info = strategy.parse_file("test.go", test_code_with_all_symbols) + + # Create a lookup dict by symbol name for easier access + # This will throw KeyError if a symbol is missing + symbol_lookup = {} + for symbol_id, symbol_info in symbols.items(): + # Extract the symbol name from the ID (format: "file.go::SymbolName") + if '::' in symbol_id: + name = symbol_id.split('::')[1] + symbol_lookup[name] = symbol_info + + # Verify package is extracted + assert file_info.package == "main" + + # Verify imports are extracted (including tricky formats and aliases) + assert "fmt" in file_info.imports + assert "strings" in file_info.imports + assert "errors" in file_info.imports + assert "example.com/project/toolkit" in file_info.imports + assert "math" in file_info.imports + assert "embed" in file_info.imports + assert "time" in file_info.imports + + # Verify all expected functions are in file_info + discovered_functions = file_info.symbols.get('functions', []) + expected_functions = ['Add', 'ComplexFunc', 'Divide', 'ProcessData', 'NewUser', 'GetFullName', 'Activate', 'IsValid'] + for func in expected_functions: + assert func in discovered_functions, f"Function '{func}' not in file_info.symbols['functions']" + + # Verify structs and interfaces are discovered (in 'classes') + discovered_classes = file_info.symbols.get('classes', []) + assert 'User' in discovered_classes + assert 'Repository' in discovered_classes + assert 'Logger' in discovered_classes + + # Verify all symbols are in lookup + assert 'Add' in symbol_lookup + assert 'ComplexFunc' in symbol_lookup + assert 'Divide' in symbol_lookup + assert 'ProcessData' in symbol_lookup + assert 'User' in symbol_lookup + assert 'NewUser' in symbol_lookup + assert 'GetFullName' in symbol_lookup + assert 'Activate' in symbol_lookup + assert 'IsValid' in symbol_lookup + assert 'Repository' in symbol_lookup + assert 'Logger' in symbol_lookup + + # Check symbol types + assert symbol_lookup['Add'].type == 'function' + assert symbol_lookup['ComplexFunc'].type == 'function' + assert symbol_lookup['Divide'].type == 'function' + assert symbol_lookup['ProcessData'].type == 'function' + assert symbol_lookup['NewUser'].type == 'function' + assert symbol_lookup['GetFullName'].type == 'method' + assert symbol_lookup['Activate'].type == 'method' + assert symbol_lookup['IsValid'].type == 'method' + assert symbol_lookup['User'].type == 'struct' + assert symbol_lookup['Repository'].type == 'interface' + assert symbol_lookup['Logger'].type == 'interface' + + # Check signatures are captured + assert symbol_lookup['Add'].signature is not None + assert 'Add' in symbol_lookup['Add'].signature + assert 'int' in symbol_lookup['Add'].signature + + assert symbol_lookup['ComplexFunc'].signature is not None + assert 'ComplexFunc' in symbol_lookup['ComplexFunc'].signature + assert '...int' in symbol_lookup['ComplexFunc'].signature or 'values' in symbol_lookup['ComplexFunc'].signature + + assert symbol_lookup['GetFullName'].signature is not None + assert 'GetFullName' in symbol_lookup['GetFullName'].signature + assert 'User' in symbol_lookup['GetFullName'].signature or '*User' in symbol_lookup['GetFullName'].signature + + # Check line numbers are correct (approximate, since we know the structure) + assert symbol_lookup['Add'].line > 10 + assert symbol_lookup['User'].line > symbol_lookup['Add'].line + assert symbol_lookup['GetFullName'].line > symbol_lookup['User'].line + + # Check that methods are associated with correct receivers + # Method signatures should contain receiver information + assert 'User' in symbol_lookup['GetFullName'].signature + assert 'User' in symbol_lookup['Activate'].signature + assert 'User' in symbol_lookup['IsValid'].signature + + # Check docstrings/comments are extracted + assert symbol_lookup['Add'].docstring == "Add returns the sum of two integers." + assert symbol_lookup['ComplexFunc'].docstring is not None + assert "complex function signature" in symbol_lookup['ComplexFunc'].docstring.lower() + assert "variadic" in symbol_lookup['ComplexFunc'].docstring.lower() + + assert symbol_lookup['User'].docstring == "User represents a user in the system." + assert symbol_lookup['NewUser'].docstring == "NewUser creates a new User instance." + + assert symbol_lookup['GetFullName'].docstring == "GetFullName returns the user's full name." + assert symbol_lookup['Activate'].docstring is not None + assert "marks the user as active" in symbol_lookup['Activate'].docstring.lower() + + assert symbol_lookup['IsValid'].docstring is not None + assert "checks if the user has valid data" in symbol_lookup['IsValid'].docstring.lower() + + assert symbol_lookup['Repository'].docstring == "Repository interface defines data access methods." + assert symbol_lookup['Logger'].docstring == "Logger is a simple interface for logging." + + # Check Go standard docstring format with parameters and returns + divide_doc = symbol_lookup['Divide'].docstring + assert divide_doc is not None + assert "divides two numbers" in divide_doc.lower() + assert "Parameters:" in divide_doc + assert "Returns:" in divide_doc + assert "- a: the dividend" in divide_doc + assert "- b: the divisor" in divide_doc + assert "error if b is zero" in divide_doc.lower() + + # Check detailed docstring with examples + process_doc = symbol_lookup['ProcessData'].docstring + assert process_doc is not None + assert "processes the given data" in process_doc.lower() + assert "Args:" in process_doc or "Parameters:" in process_doc.lower() + assert "Returns:" in process_doc + assert "Example:" in process_doc + assert "ProcessData" in process_doc diff --git a/reference/code-index-mcp-master/tests/strategies/test_python_discovery.py b/reference/code-index-mcp-master/tests/strategies/test_python_discovery.py new file mode 100644 index 00000000..d92f52ec --- /dev/null +++ b/reference/code-index-mcp-master/tests/strategies/test_python_discovery.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +Test for Python symbol discovery including all symbol types. +""" +import pytest +from textwrap import dedent + +from code_index_mcp.indexing.strategies.python_strategy import PythonParsingStrategy + + +@pytest.fixture +def test_code_with_all_symbols(): + """Fixture with variables, constants, functions, classes and methods.""" + return ''' +CONSTANT = 42 +variable = 'hello' + +def sync_function(): + """A regular synchronous function.""" + return "sync result" + +async def async_function(): + """An asynchronous function.""" + return "async result" + +def top_level_function(x, y): + """Function without type hints.""" + return x + y + +def function_with_types(name: str, age: int, active: bool = True) -> dict: + """ + Function with type hints and default values. + + Args: + name: The person's name + age: The person's age + active: Whether the person is active + + Returns: + A dictionary with person info + """ + return {"name": name, "age": age, "active": active} + +def complex_function(items: list[str], *args: int, callback=None, **kwargs: str) -> tuple[int, str]: + """Function with complex signature including *args and **kwargs.""" + return len(items), str(args) + +class TestClass: + """A test class with various methods.""" + CLASS_VAR = 123 + + def __init__(self, value: int): + """Initialize with a value.""" + self.value = value + + def sync_method(self): + """A regular synchronous method.""" + return "sync method result" + + async def async_method(self): + """An asynchronous method.""" + return "async method result" + + def method(self): + return self.value + + def typed_method(self, x: float, y: float) -> float: + """Method with type hints. + + Returns the sum of x and y. + """ + return x + y +''' + + +def test_python_symbol_discovery(test_code_with_all_symbols): + """Test that all Python symbol types are correctly discovered.""" + strategy = PythonParsingStrategy() + symbols, file_info = strategy.parse_file("test.py", test_code_with_all_symbols) + + # Create a lookup dict by symbol name for easier access + # This will throw KeyError if a symbol is missing + symbol_lookup = {} + for symbol_id, symbol_info in symbols.items(): + # Extract the symbol name from the ID (format: "file.py::SymbolName") + if '::' in symbol_id: + name = symbol_id.split('::')[1] + symbol_lookup[name] = symbol_info + + # Verify all expected functions are in file_info + discovered_functions = file_info.symbols.get('functions', []) + expected_functions = ['sync_function', 'async_function', 'top_level_function', + 'function_with_types', 'complex_function'] + for func in expected_functions: + assert func in discovered_functions, f"Function '{func}' not in file_info.symbols['functions']" + + # Verify all expected methods are discovered + expected_methods = ['TestClass.__init__', 'TestClass.sync_method', + 'TestClass.async_method', 'TestClass.method', 'TestClass.typed_method'] + for method in expected_methods: + assert method in symbol_lookup, f"Method '{method}' not found in symbols" + + # Verify class is discovered + assert 'TestClass' in file_info.symbols.get('classes', []) + assert 'TestClass' in symbol_lookup + + # Check symbol types + assert symbol_lookup['sync_function'].type == 'function' + assert symbol_lookup['async_function'].type == 'function' + assert symbol_lookup['top_level_function'].type == 'function' + assert symbol_lookup['function_with_types'].type == 'function' + assert symbol_lookup['complex_function'].type == 'function' + assert symbol_lookup['TestClass'].type == 'class' + assert symbol_lookup['TestClass.__init__'].type == 'method' + assert symbol_lookup['TestClass.sync_method'].type == 'method' + assert symbol_lookup['TestClass.async_method'].type == 'method' + assert symbol_lookup['TestClass.method'].type == 'method' + assert symbol_lookup['TestClass.typed_method'].type == 'method' + + # Check docstrings explicitly + assert symbol_lookup['sync_function'].docstring == "A regular synchronous function." + assert symbol_lookup['async_function'].docstring == "An asynchronous function." + assert symbol_lookup['top_level_function'].docstring == "Function without type hints." + + expected_docstring = dedent(""" + Function with type hints and default values. + + Args: + name: The person's name + age: The person's age + active: Whether the person is active + + Returns: + A dictionary with person info + """).strip() + assert symbol_lookup['function_with_types'].docstring == expected_docstring + + assert symbol_lookup['complex_function'].docstring == "Function with complex signature including *args and **kwargs." + assert symbol_lookup['TestClass.__init__'].docstring == "Initialize with a value." + assert symbol_lookup['TestClass.sync_method'].docstring == "A regular synchronous method." + assert symbol_lookup['TestClass.async_method'].docstring == "An asynchronous method." + assert symbol_lookup['TestClass.method'].docstring is None + + expected_typed_method_docstring = dedent(""" + Method with type hints. + + Returns the sum of x and y. + """).strip() + assert symbol_lookup['TestClass.typed_method'].docstring == expected_typed_method_docstring + assert symbol_lookup['TestClass'].docstring == "A test class with various methods." + + # Check signatures explicitly + assert symbol_lookup['sync_function'].signature == "def sync_function():" + assert symbol_lookup['async_function'].signature == "def async_function():" + assert symbol_lookup['top_level_function'].signature == "def top_level_function(x, y):" + assert symbol_lookup['function_with_types'].signature == "def function_with_types(name, age, active):" + assert symbol_lookup['complex_function'].signature == "def complex_function(items, *args, **kwargs):" + assert symbol_lookup['TestClass.__init__'].signature == "def __init__(self, value):" + assert symbol_lookup['TestClass.sync_method'].signature == "def sync_method(self):" + assert symbol_lookup['TestClass.async_method'].signature == "def async_method(self):" + assert symbol_lookup['TestClass.method'].signature == "def method(self):" + assert symbol_lookup['TestClass.typed_method'].signature == "def typed_method(self, x, y):" diff --git a/reference/code-index-mcp-master/tests/utils/test_validation_pagination.py b/reference/code-index-mcp-master/tests/utils/test_validation_pagination.py new file mode 100644 index 00000000..cc95713e --- /dev/null +++ b/reference/code-index-mcp-master/tests/utils/test_validation_pagination.py @@ -0,0 +1,22 @@ +"""Tests for pagination validation helper.""" +from pathlib import Path as _TestPath +import sys + +ROOT = _TestPath(__file__).resolve().parents[2] +SRC_PATH = ROOT / 'src' +if str(SRC_PATH) not in sys.path: + sys.path.insert(0, str(SRC_PATH)) + +from code_index_mcp.utils.validation import ValidationHelper + + +def test_validate_pagination_accepts_valid_values(): + assert ValidationHelper.validate_pagination(0, None) is None + assert ValidationHelper.validate_pagination(5, 10) is None + + +def test_validate_pagination_rejects_invalid_values(): + assert ValidationHelper.validate_pagination(-1, None) == "start_index cannot be negative" + assert ValidationHelper.validate_pagination(0, 0) == "max_results must be greater than zero when provided" + assert ValidationHelper.validate_pagination(0, "a") == "max_results must be an integer when provided" + assert ValidationHelper.validate_pagination("a", None) == "start_index must be an integer" diff --git a/reference/code-index-mcp-master/uv.lock b/reference/code-index-mcp-master/uv.lock new file mode 100644 index 00000000..4b9de119 --- /dev/null +++ b/reference/code-index-mcp-master/uv.lock @@ -0,0 +1,962 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283 }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504 }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811 }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402 }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217 }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079 }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475 }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829 }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211 }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036 }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184 }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344 }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560 }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613 }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374 }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597 }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574 }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971 }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972 }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078 }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076 }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820 }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635 }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "code-index-mcp" +version = "2.9.4" +source = { editable = "." } +dependencies = [ + { name = "mcp" }, + { name = "msgpack" }, + { name = "pathspec" }, + { name = "tree-sitter" }, + { name = "tree-sitter-java" }, + { name = "tree-sitter-javascript" }, + { name = "tree-sitter-typescript" }, + { name = "tree-sitter-zig" }, + { name = "watchdog" }, +] + +[package.metadata] +requires-dist = [ + { name = "mcp", specifier = ">=1.21.0,<2.0.0" }, + { name = "msgpack", specifier = ">=1.0.0" }, + { name = "pathspec", specifier = ">=0.12.1" }, + { name = "tree-sitter", specifier = ">=0.20.0" }, + { name = "tree-sitter-java", specifier = ">=0.20.0" }, + { name = "tree-sitter-javascript", specifier = ">=0.20.0" }, + { name = "tree-sitter-typescript", specifier = ">=0.20.0" }, + { name = "tree-sitter-zig", specifier = ">=0.20.0" }, + { name = "watchdog", specifier = ">=3.0.0" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004 }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667 }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807 }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615 }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800 }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707 }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541 }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464 }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838 }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596 }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782 }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381 }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988 }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451 }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007 }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012 }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728 }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078 }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460 }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237 }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344 }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564 }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415 }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457 }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074 }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569 }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941 }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339 }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315 }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331 }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248 }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089 }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029 }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222 }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280 }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958 }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714 }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970 }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236 }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642 }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126 }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573 }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695 }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720 }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740 }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163 }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474 }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132 }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992 }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944 }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957 }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447 }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, +] + +[[package]] +name = "mcp" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/54/dd2330ef4611c27ae59124820863c34e1d3edb1133c58e6375e2d938c9c5/mcp-1.21.0.tar.gz", hash = "sha256:bab0a38e8f8c48080d787233343f8d301b0e1e95846ae7dead251b2421d99855", size = 452697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/47/850b6edc96c03bd44b00de9a0ca3c1cc71e0ba1cd5822955bc9e4eb3fad3/mcp-1.21.0-py3-none-any.whl", hash = "sha256:598619e53eb0b7a6513db38c426b28a4bdf57496fed04332100d2c56acade98b", size = 173672 }, +] + +[[package]] +name = "msgpack" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/52/f30da112c1dc92cf64f57d08a273ac771e7b29dea10b4b30369b2d7e8546/msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed", size = 81799 }, + { url = "https://files.pythonhosted.org/packages/e4/35/7bfc0def2f04ab4145f7f108e3563f9b4abae4ab0ed78a61f350518cc4d2/msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8", size = 78278 }, + { url = "https://files.pythonhosted.org/packages/e8/c5/df5d6c1c39856bc55f800bf82778fd4c11370667f9b9e9d51b2f5da88f20/msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2", size = 402805 }, + { url = "https://files.pythonhosted.org/packages/20/8e/0bb8c977efecfe6ea7116e2ed73a78a8d32a947f94d272586cf02a9757db/msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4", size = 408642 }, + { url = "https://files.pythonhosted.org/packages/59/a1/731d52c1aeec52006be6d1f8027c49fdc2cfc3ab7cbe7c28335b2910d7b6/msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0", size = 395143 }, + { url = "https://files.pythonhosted.org/packages/2b/92/b42911c52cda2ba67a6418ffa7d08969edf2e760b09015593c8a8a27a97d/msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26", size = 395986 }, + { url = "https://files.pythonhosted.org/packages/61/dc/8ae165337e70118d4dab651b8b562dd5066dd1e6dd57b038f32ebc3e2f07/msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75", size = 402682 }, + { url = "https://files.pythonhosted.org/packages/58/27/555851cb98dcbd6ce041df1eacb25ac30646575e9cd125681aa2f4b1b6f1/msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338", size = 406368 }, + { url = "https://files.pythonhosted.org/packages/d4/64/39a26add4ce16f24e99eabb9005e44c663db00e3fce17d4ae1ae9d61df99/msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd", size = 65004 }, + { url = "https://files.pythonhosted.org/packages/7d/18/73dfa3e9d5d7450d39debde5b0d848139f7de23bd637a4506e36c9800fd6/msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8", size = 71548 }, + { url = "https://files.pythonhosted.org/packages/7f/83/97f24bf9848af23fe2ba04380388216defc49a8af6da0c28cc636d722502/msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558", size = 82728 }, + { url = "https://files.pythonhosted.org/packages/aa/7f/2eaa388267a78401f6e182662b08a588ef4f3de6f0eab1ec09736a7aaa2b/msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d", size = 79279 }, + { url = "https://files.pythonhosted.org/packages/f8/46/31eb60f4452c96161e4dfd26dbca562b4ec68c72e4ad07d9566d7ea35e8a/msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0", size = 423859 }, + { url = "https://files.pythonhosted.org/packages/45/16/a20fa8c32825cc7ae8457fab45670c7a8996d7746ce80ce41cc51e3b2bd7/msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f", size = 429975 }, + { url = "https://files.pythonhosted.org/packages/86/ea/6c958e07692367feeb1a1594d35e22b62f7f476f3c568b002a5ea09d443d/msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704", size = 413528 }, + { url = "https://files.pythonhosted.org/packages/75/05/ac84063c5dae79722bda9f68b878dc31fc3059adb8633c79f1e82c2cd946/msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2", size = 413338 }, + { url = "https://files.pythonhosted.org/packages/69/e8/fe86b082c781d3e1c09ca0f4dacd457ede60a13119b6ce939efe2ea77b76/msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2", size = 422658 }, + { url = "https://files.pythonhosted.org/packages/3b/2b/bafc9924df52d8f3bb7c00d24e57be477f4d0f967c0a31ef5e2225e035c7/msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752", size = 427124 }, + { url = "https://files.pythonhosted.org/packages/a2/3b/1f717e17e53e0ed0b68fa59e9188f3f610c79d7151f0e52ff3cd8eb6b2dc/msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295", size = 65016 }, + { url = "https://files.pythonhosted.org/packages/48/45/9d1780768d3b249accecc5a38c725eb1e203d44a191f7b7ff1941f7df60c/msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458", size = 72267 }, + { url = "https://files.pythonhosted.org/packages/e3/26/389b9c593eda2b8551b2e7126ad3a06af6f9b44274eb3a4f054d48ff7e47/msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238", size = 82359 }, + { url = "https://files.pythonhosted.org/packages/ab/65/7d1de38c8a22cf8b1551469159d4b6cf49be2126adc2482de50976084d78/msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157", size = 79172 }, + { url = "https://files.pythonhosted.org/packages/0f/bd/cacf208b64d9577a62c74b677e1ada005caa9b69a05a599889d6fc2ab20a/msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce", size = 425013 }, + { url = "https://files.pythonhosted.org/packages/4d/ec/fd869e2567cc9c01278a736cfd1697941ba0d4b81a43e0aa2e8d71dab208/msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a", size = 426905 }, + { url = "https://files.pythonhosted.org/packages/55/2a/35860f33229075bce803a5593d046d8b489d7ba2fc85701e714fc1aaf898/msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c", size = 407336 }, + { url = "https://files.pythonhosted.org/packages/8c/16/69ed8f3ada150bf92745fb4921bd621fd2cdf5a42e25eb50bcc57a5328f0/msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b", size = 409485 }, + { url = "https://files.pythonhosted.org/packages/c6/b6/0c398039e4c6d0b2e37c61d7e0e9d13439f91f780686deb8ee64ecf1ae71/msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef", size = 412182 }, + { url = "https://files.pythonhosted.org/packages/b8/d0/0cf4a6ecb9bc960d624c93effaeaae75cbf00b3bc4a54f35c8507273cda1/msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a", size = 419883 }, + { url = "https://files.pythonhosted.org/packages/62/83/9697c211720fa71a2dfb632cad6196a8af3abea56eece220fde4674dc44b/msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c", size = 65406 }, + { url = "https://files.pythonhosted.org/packages/c0/23/0abb886e80eab08f5e8c485d6f13924028602829f63b8f5fa25a06636628/msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4", size = 72558 }, + { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677 }, + { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603 }, + { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504 }, + { url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749 }, + { url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458 }, + { url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976 }, + { url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607 }, + { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172 }, + { url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347 }, + { url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298 }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475 }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815 }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567 }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442 }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956 }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253 }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050 }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178 }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833 }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156 }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378 }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622 }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351 }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363 }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615 }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369 }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218 }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951 }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428 }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009 }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432 }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103 }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557 }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031 }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308 }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930 }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, +] + +[[package]] +name = "rpds-py" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/f8/13bb772dc7cbf2c3c5b816febc34fa0cb2c64a08e0569869585684ce6631/rpds_py-0.28.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a", size = 362820 }, + { url = "https://files.pythonhosted.org/packages/84/91/6acce964aab32469c3dbe792cb041a752d64739c534e9c493c701ef0c032/rpds_py-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207", size = 348499 }, + { url = "https://files.pythonhosted.org/packages/f1/93/c05bb1f4f5e0234db7c4917cb8dd5e2e0a9a7b26dc74b1b7bee3c9cfd477/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba", size = 379356 }, + { url = "https://files.pythonhosted.org/packages/5c/37/e292da436f0773e319753c567263427cdf6c645d30b44f09463ff8216cda/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85", size = 390151 }, + { url = "https://files.pythonhosted.org/packages/76/87/a4e3267131616e8faf10486dc00eaedf09bd61c87f01e5ef98e782ee06c9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d", size = 524831 }, + { url = "https://files.pythonhosted.org/packages/e1/c8/4a4ca76f0befae9515da3fad11038f0fce44f6bb60b21fe9d9364dd51fb0/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7", size = 404687 }, + { url = "https://files.pythonhosted.org/packages/6a/65/118afe854424456beafbbebc6b34dcf6d72eae3a08b4632bc4220f8240d9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa", size = 382683 }, + { url = "https://files.pythonhosted.org/packages/f7/bc/0625064041fb3a0c77ecc8878c0e8341b0ae27ad0f00cf8f2b57337a1e63/rpds_py-0.28.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476", size = 398927 }, + { url = "https://files.pythonhosted.org/packages/5d/1a/fed7cf2f1ee8a5e4778f2054153f2cfcf517748875e2f5b21cf8907cd77d/rpds_py-0.28.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04", size = 411590 }, + { url = "https://files.pythonhosted.org/packages/c1/64/a8e0f67fa374a6c472dbb0afdaf1ef744724f165abb6899f20e2f1563137/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8", size = 559843 }, + { url = "https://files.pythonhosted.org/packages/a9/ea/e10353f6d7c105be09b8135b72787a65919971ae0330ad97d87e4e199880/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4", size = 584188 }, + { url = "https://files.pythonhosted.org/packages/18/b0/a19743e0763caf0c89f6fc6ba6fbd9a353b24ffb4256a492420c5517da5a/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457", size = 550052 }, + { url = "https://files.pythonhosted.org/packages/de/bc/ec2c004f6c7d6ab1e25dae875cdb1aee087c3ebed5b73712ed3000e3851a/rpds_py-0.28.0-cp310-cp310-win32.whl", hash = "sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e", size = 215110 }, + { url = "https://files.pythonhosted.org/packages/6c/de/4ce8abf59674e17187023933547d2018363e8fc76ada4f1d4d22871ccb6e/rpds_py-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8", size = 223850 }, + { url = "https://files.pythonhosted.org/packages/a6/34/058d0db5471c6be7bef82487ad5021ff8d1d1d27794be8730aad938649cf/rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296", size = 362344 }, + { url = "https://files.pythonhosted.org/packages/5d/67/9503f0ec8c055a0782880f300c50a2b8e5e72eb1f94dfc2053da527444dd/rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27", size = 348440 }, + { url = "https://files.pythonhosted.org/packages/68/2e/94223ee9b32332a41d75b6f94b37b4ce3e93878a556fc5f152cbd856a81f/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c", size = 379068 }, + { url = "https://files.pythonhosted.org/packages/b4/25/54fd48f9f680cfc44e6a7f39a5fadf1d4a4a1fd0848076af4a43e79f998c/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205", size = 390518 }, + { url = "https://files.pythonhosted.org/packages/1b/85/ac258c9c27f2ccb1bd5d0697e53a82ebcf8088e3186d5d2bf8498ee7ed44/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95", size = 525319 }, + { url = "https://files.pythonhosted.org/packages/40/cb/c6734774789566d46775f193964b76627cd5f42ecf246d257ce84d1912ed/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9", size = 404896 }, + { url = "https://files.pythonhosted.org/packages/1f/53/14e37ce83202c632c89b0691185dca9532288ff9d390eacae3d2ff771bae/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2", size = 382862 }, + { url = "https://files.pythonhosted.org/packages/6a/83/f3642483ca971a54d60caa4449f9d6d4dbb56a53e0072d0deff51b38af74/rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0", size = 398848 }, + { url = "https://files.pythonhosted.org/packages/44/09/2d9c8b2f88e399b4cfe86efdf2935feaf0394e4f14ab30c6c5945d60af7d/rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e", size = 412030 }, + { url = "https://files.pythonhosted.org/packages/dd/f5/e1cec473d4bde6df1fd3738be8e82d64dd0600868e76e92dfeaebbc2d18f/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67", size = 559700 }, + { url = "https://files.pythonhosted.org/packages/8d/be/73bb241c1649edbf14e98e9e78899c2c5e52bbe47cb64811f44d2cc11808/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d", size = 584581 }, + { url = "https://files.pythonhosted.org/packages/9c/9c/ffc6e9218cd1eb5c2c7dbd276c87cd10e8c2232c456b554169eb363381df/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6", size = 549981 }, + { url = "https://files.pythonhosted.org/packages/5f/50/da8b6d33803a94df0149345ee33e5d91ed4d25fc6517de6a25587eae4133/rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c", size = 214729 }, + { url = "https://files.pythonhosted.org/packages/12/fd/b0f48c4c320ee24c8c20df8b44acffb7353991ddf688af01eef5f93d7018/rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa", size = 223977 }, + { url = "https://files.pythonhosted.org/packages/b4/21/c8e77a2ac66e2ec4e21f18a04b4e9a0417ecf8e61b5eaeaa9360a91713b4/rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120", size = 217326 }, + { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439 }, + { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170 }, + { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838 }, + { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299 }, + { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000 }, + { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746 }, + { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379 }, + { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280 }, + { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365 }, + { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573 }, + { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973 }, + { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800 }, + { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954 }, + { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844 }, + { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624 }, + { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235 }, + { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241 }, + { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079 }, + { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151 }, + { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520 }, + { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699 }, + { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720 }, + { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096 }, + { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465 }, + { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832 }, + { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230 }, + { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268 }, + { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100 }, + { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759 }, + { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326 }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736 }, + { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677 }, + { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847 }, + { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800 }, + { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827 }, + { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471 }, + { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578 }, + { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447 }, + { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385 }, + { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642 }, + { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507 }, + { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376 }, + { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907 }, + { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830 }, + { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819 }, + { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127 }, + { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767 }, + { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585 }, + { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828 }, + { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509 }, + { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014 }, + { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410 }, + { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593 }, + { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925 }, + { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444 }, + { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968 }, + { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876 }, + { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506 }, + { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433 }, + { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601 }, + { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039 }, + { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407 }, + { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172 }, + { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020 }, + { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451 }, + { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355 }, + { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146 }, + { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656 }, + { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782 }, + { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671 }, + { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749 }, + { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233 }, + { url = "https://files.pythonhosted.org/packages/ae/bc/b43f2ea505f28119bd551ae75f70be0c803d2dbcd37c1b3734909e40620b/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16", size = 363913 }, + { url = "https://files.pythonhosted.org/packages/28/f2/db318195d324c89a2c57dc5195058cbadd71b20d220685c5bd1da79ee7fe/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d", size = 350452 }, + { url = "https://files.pythonhosted.org/packages/ae/f2/1391c819b8573a4898cedd6b6c5ec5bc370ce59e5d6bdcebe3c9c1db4588/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db", size = 380957 }, + { url = "https://files.pythonhosted.org/packages/5a/5c/e5de68ee7eb7248fce93269833d1b329a196d736aefb1a7481d1e99d1222/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7", size = 391919 }, + { url = "https://files.pythonhosted.org/packages/fb/4f/2376336112cbfeb122fd435d608ad8d5041b3aed176f85a3cb32c262eb80/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78", size = 528541 }, + { url = "https://files.pythonhosted.org/packages/68/53/5ae232e795853dd20da7225c5dd13a09c0a905b1a655e92bdf8d78a99fd9/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec", size = 405629 }, + { url = "https://files.pythonhosted.org/packages/b9/2d/351a3b852b683ca9b6b8b38ed9efb2347596973849ba6c3a0e99877c10aa/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72", size = 384123 }, + { url = "https://files.pythonhosted.org/packages/e0/15/870804daa00202728cc91cb8e2385fa9f1f4eb49857c49cfce89e304eae6/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27", size = 400923 }, + { url = "https://files.pythonhosted.org/packages/53/25/3706b83c125fa2a0bccceac951de3f76631f6bd0ee4d02a0ed780712ef1b/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316", size = 413767 }, + { url = "https://files.pythonhosted.org/packages/ef/f9/ce43dbe62767432273ed2584cef71fef8411bddfb64125d4c19128015018/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912", size = 561530 }, + { url = "https://files.pythonhosted.org/packages/46/c9/ffe77999ed8f81e30713dd38fd9ecaa161f28ec48bb80fa1cd9118399c27/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829", size = 585453 }, + { url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sse-starlette" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, +] + +[[package]] +name = "starlette" +version = "0.46.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, +] + +[[package]] +name = "tree-sitter" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/2b/02a642e67605b9dd59986b00d13a076044dede04025a243f0592ac79d68c/tree-sitter-0.25.1.tar.gz", hash = "sha256:cd761ad0e4d1fc88a4b1b8083bae06d4f973acf6f5f29bbf13ea9609c1dec9c1", size = 177874 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/6c/6160ca15926d11a6957d8bee887f477f3c1d9bc5272c863affc0b50b9cff/tree_sitter-0.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a15d62ffdb095d509bda8c140c1ddd0cc80f0c67f92b87fcc96cd242dc0c71ea", size = 146692 }, + { url = "https://files.pythonhosted.org/packages/81/4a/e5eb39fe73a514a13bf94acee97925de296d673dace00557763cbbdc938f/tree_sitter-0.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d938f0a1ffad1206a1a569b0501345eeca81cae0a4487bb485e53768b02f24e", size = 141015 }, + { url = "https://files.pythonhosted.org/packages/63/22/c8e3ba245e5cdb8c951482028a7ee99d141302047b708dc9d670f0fafd85/tree_sitter-0.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba8cea296de5dcb384b9a15cf526985ac8339c81da51c7e29a251d82071f5ee9", size = 599462 }, + { url = "https://files.pythonhosted.org/packages/c2/91/c866c3d278ee86354fd81fd055b5d835c510b0e9af07e1cf7e48e2f946b0/tree_sitter-0.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:387fd2bd8657d69e877618dc199c18e2d6fe073b8f5c59e23435f3baee4ee10a", size = 627062 }, + { url = "https://files.pythonhosted.org/packages/90/96/ac010f72778dae60381ab5fcca9651ac72647d582db0b027ca6c56116920/tree_sitter-0.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:afa49e51f82b58ae2c1291d6b79ca31e0fb36c04bd9a20d89007472edfb70136", size = 623788 }, + { url = "https://files.pythonhosted.org/packages/0e/29/190bdfd54a564a2e43a702884ad5679f4578c481a46161f9f335dd390a70/tree_sitter-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:77be45f666adf284914510794b41100decccd71dba88010c03dc2bb0d653acec", size = 127253 }, + { url = "https://files.pythonhosted.org/packages/da/60/7daca5ccf65fb204c9f2cc2907db6aeaf1cb42aa605427580c17a38a53b3/tree_sitter-0.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:72badac2de4e81ae0df5efe14ec5003bd4df3e48e7cf84dbd9df3a54599ba371", size = 113930 }, + { url = "https://files.pythonhosted.org/packages/17/dc/0dabb75d249108fb9062d6e9e791e4ad8e9ae5c095e06dd8af770bc07902/tree_sitter-0.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33a8fbaeb2b5049cf5318306ab8b16ab365828b2b21ee13678c29e0726a1d27a", size = 146696 }, + { url = "https://files.pythonhosted.org/packages/da/d0/b7305a05d65dbcfce7a97a93252bf7384f09800866e9de55a625c76e0257/tree_sitter-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:797bbbc686d8d3722d25ee0108ad979bda6ad3e1025859ce2ee290e517816bd4", size = 141014 }, + { url = "https://files.pythonhosted.org/packages/84/d0/d0d8bd13c44ef6379499712a3f5e3930e7db11e5c8eb2af8655e288597a3/tree_sitter-0.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629fc2ae3f5954b0f6a7b42ee3fcd8f34b68ea161e9f02fa5bf709cbbac996d3", size = 604339 }, + { url = "https://files.pythonhosted.org/packages/c5/13/22869a6da25ffe2dfff922712605e72a9c3481109a93f4218bea1bc65f35/tree_sitter-0.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4257018c42a33a7935a5150d678aac05c6594347d6a6e6dbdf7e2ef4ae985213", size = 631593 }, + { url = "https://files.pythonhosted.org/packages/ec/0c/f4590fc08422768fc57456a85c932888a02e7a13540574859308611be1cf/tree_sitter-0.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4027854c9feee2a3bb99642145ba04ce95d75bd17e292911c93a488cb28d0a04", size = 629265 }, + { url = "https://files.pythonhosted.org/packages/a7/a8/ee9305ce9a7417715cbf038fdcc4fdb6042e30065c9837bdcf36be440388/tree_sitter-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:183faaedcee5f0a3ba39257fa81749709d5eb7cf92c2c050b36ff38468d1774c", size = 127210 }, + { url = "https://files.pythonhosted.org/packages/48/64/6a39882f534373873ef3dba8a1a8f47dc3bfb39ee63784eac2e789b404c4/tree_sitter-0.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:6a3800235535a2532ce392ed0d8e6f698ee010e73805bdeac2f249da8246bab6", size = 113928 }, + { url = "https://files.pythonhosted.org/packages/45/79/6dea0c098879d99f41ba919da1ea46e614fb4bf9c4d591450061aeec6fcb/tree_sitter-0.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9362a202144075b54f7c9f07e0b0e44a61eed7ee19e140c506b9e64c1d21ed58", size = 146928 }, + { url = "https://files.pythonhosted.org/packages/15/30/8002f4e76c7834a6101895ff7524ea29ab4f1f1da1270260ef52e2319372/tree_sitter-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:593f22529f34dd04de02f56ea6d7c2c8ec99dfab25b58be893247c1090dedd60", size = 140802 }, + { url = "https://files.pythonhosted.org/packages/38/ec/d297ad9d4a4b26f551a5ca49afe48fdbcb20f058c2eff8d8463ad6c0eed1/tree_sitter-0.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb6849f76e1cbfa223303fa680da533d452e378d5fe372598e4752838ca7929", size = 606762 }, + { url = "https://files.pythonhosted.org/packages/4a/1c/05a623cfb420b10d5f782d4ec064cf00fbfa9c21b8526ca4fd042f80acff/tree_sitter-0.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:034d4544bb0f82e449033d76dd083b131c3f9ecb5e37d3475f80ae55e8f382bd", size = 634632 }, + { url = "https://files.pythonhosted.org/packages/c5/e0/f05fd5a2331c16d428efb8eef32dfb80dc6565438146e34e9a235ecd7925/tree_sitter-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46a9b721560070f2f980105266e28a17d3149485582cdba14d66dca14692e932", size = 630756 }, + { url = "https://files.pythonhosted.org/packages/b2/fc/79f3c5d53d1721b95ab6cda0368192a4f1d367e3a5ff7ac21d77e9841782/tree_sitter-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:9a5c522b1350a626dc1cbc5dc203133caeaa114d3f65e400445e8b02f18b343b", size = 127157 }, + { url = "https://files.pythonhosted.org/packages/24/b7/07c4e3f71af0096db6c2ecd83e7d61584e3891c79cb39b208082312d1d60/tree_sitter-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:43e7b8e83f9fc29ca62e7d2aa8c38e3fa806ff3fc65e0d501d18588dc1509888", size = 113910 }, + { url = "https://files.pythonhosted.org/packages/3f/d3/bfb08aab9c7daed2715f303cc017329e3512bb77678cc28829681decadd2/tree_sitter-0.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae1eebc175e6a50b38b0e0385cdc26e92ac0bff9b32ee1c0619bbbf6829d57ea", size = 146920 }, + { url = "https://files.pythonhosted.org/packages/f9/36/7f897c50489c38665255579646fca8191e1b9e5a29ac9cf11022e42e1e2b/tree_sitter-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e0ae03c4f132f1bffb2bc40b1bb28742785507da693ab04da8531fe534ada9c", size = 140782 }, + { url = "https://files.pythonhosted.org/packages/16/e6/85012113899296b8e0789ae94f562d3971d7d3df989e8bec6128749394e1/tree_sitter-0.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acf571758be0a71046a61a0936cb815f15b13e0ae7ec6d08398e4aa1560b371d", size = 607590 }, + { url = "https://files.pythonhosted.org/packages/49/93/605b08dc4cf76d08cfacebc30a88467c6526ea5c94592c25240518e38b71/tree_sitter-0.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:632910847e3f8ae35841f92cba88a9a1b8bc56ecc1514a5affebf7951fa0fc0a", size = 635553 }, + { url = "https://files.pythonhosted.org/packages/ce/27/123667f756bb32168507c940db9040104c606fbb0214397d3c20cf985073/tree_sitter-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a99ecef7771afb118b2a8435c8ba67ea7a085c60d5d33dc0a4794ed882e5f7df", size = 630844 }, + { url = "https://files.pythonhosted.org/packages/2f/53/180b0ed74153a3c9a23967f54774d5930c2e0b67671ae4ca0d4d35ba18ac/tree_sitter-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:c1d6393454d1f9d4195c74e40a487640cd4390cd4aee90837485f932a1a0f40c", size = 127159 }, + { url = "https://files.pythonhosted.org/packages/32/fb/b8b7b5122ac4a80cd689a5023f2416910e10f9534ace1cdf0020a315d40d/tree_sitter-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:c1d2dbf7d12426b71ff49739f599c355f4de338a5c0ab994de2a1d290f6e0b20", size = 113920 }, + { url = "https://files.pythonhosted.org/packages/70/8c/cb851da552baf4215baf96443e5e9e39095083a95bc05c4444e640fe0fe8/tree_sitter-0.25.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:32cee52264d9ecf98885fcac0185ac63e16251b31dd8b4a3b8d8071173405f8f", size = 146775 }, + { url = "https://files.pythonhosted.org/packages/f3/59/002c89df1e8f1664b82023e5d0c06de97fff5c2a2e33dce1a241c8909758/tree_sitter-0.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae024d8ccfef51e61c44a81af7a48670601430701c24f450bea10f4b4effd8d1", size = 140787 }, + { url = "https://files.pythonhosted.org/packages/39/48/c9e6deb88f3c7f16963ef205e5b8e3ea7f5effd048b4515d09738c7b032b/tree_sitter-0.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d025c56c393cea660df9ef33ca60329952a1f8ee6212d21b2b390dfec08a3874", size = 609173 }, + { url = "https://files.pythonhosted.org/packages/53/a8/b782576d7ea081a87285d974005155da03b6d0c66283fe1e3a5e0dd4bd98/tree_sitter-0.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:044aa23ea14f337809821bea7467f33f4c6d351739dca76ba0cbe4d0154d8662", size = 635994 }, + { url = "https://files.pythonhosted.org/packages/70/0a/c5b6c9cdb7bd4bf0c3d2bd494fcf356acc53f8e63007dc2a836d95bbe964/tree_sitter-0.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1863d96704eb002df4ad3b738294ae8bd5dcf8cefb715da18bff6cb2d33d978e", size = 630944 }, + { url = "https://files.pythonhosted.org/packages/12/2a/d0b097157c2d487f5e6293dae2c106ec9ede792a6bb780249e81432e754d/tree_sitter-0.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:a40a481e28e1afdbc455932d61e49ffd4163aafa83f4a3deb717524a7786197e", size = 130831 }, + { url = "https://files.pythonhosted.org/packages/ce/33/3591e7b22dd49f46ae4fdee1db316ecefd0486cae880c5b497a55f0ccb24/tree_sitter-0.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:f7b68f584336b39b2deab9896b629dddc3c784170733d3409f01fe825e9c04eb", size = 117376 }, +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/dc/eb9c8f96304e5d8ae1663126d89967a622a80937ad2909903569ccb7ec8f/tree_sitter_java-0.23.5.tar.gz", hash = "sha256:f5cd57b8f1270a7f0438878750d02ccc79421d45cca65ff284f1527e9ef02e38", size = 138121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/21/b3399780b440e1567a11d384d0ebb1aea9b642d0d98becf30fa55c0e3a3b/tree_sitter_java-0.23.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:355ce0308672d6f7013ec913dee4a0613666f4cda9044a7824240d17f38209df", size = 58926 }, + { url = "https://files.pythonhosted.org/packages/57/ef/6406b444e2a93bc72a04e802f4107e9ecf04b8de4a5528830726d210599c/tree_sitter_java-0.23.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:24acd59c4720dedad80d548fe4237e43ef2b7a4e94c8549b0ca6e4c4d7bf6e69", size = 62288 }, + { url = "https://files.pythonhosted.org/packages/4e/6c/74b1c150d4f69c291ab0b78d5dd1b59712559bbe7e7daf6d8466d483463f/tree_sitter_java-0.23.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9401e7271f0b333df39fc8a8336a0caf1b891d9a2b89ddee99fae66b794fc5b7", size = 85533 }, + { url = "https://files.pythonhosted.org/packages/29/09/e0d08f5c212062fd046db35c1015a2621c2631bc8b4aae5740d7adb276ad/tree_sitter_java-0.23.5-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370b204b9500b847f6d0c5ad584045831cee69e9a3e4d878535d39e4a7e4c4f1", size = 84033 }, + { url = "https://files.pythonhosted.org/packages/43/56/7d06b23ddd09bde816a131aa504ee11a1bbe87c6b62ab9b2ed23849a3382/tree_sitter_java-0.23.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aae84449e330363b55b14a2af0585e4e0dae75eb64ea509b7e5b0e1de536846a", size = 82564 }, + { url = "https://files.pythonhosted.org/packages/da/d6/0528c7e1e88a18221dbd8ccee3825bf274b1fa300f745fd74eb343878043/tree_sitter_java-0.23.5-cp39-abi3-win_amd64.whl", hash = "sha256:1ee45e790f8d31d416bc84a09dac2e2c6bc343e89b8a2e1d550513498eedfde7", size = 60650 }, + { url = "https://files.pythonhosted.org/packages/72/57/5bab54d23179350356515526fff3cc0f3ac23bfbc1a1d518a15978d4880e/tree_sitter_java-0.23.5-cp39-abi3-win_arm64.whl", hash = "sha256:402efe136104c5603b429dc26c7e75ae14faaca54cfd319ecc41c8f2534750f4", size = 59059 }, +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/dc/1c55c33cc6bbe754359b330534cf9f261c1b9b2c26ddf23aef3c5fa67759/tree_sitter_javascript-0.23.1.tar.gz", hash = "sha256:b2059ce8b150162cda05a457ca3920450adbf915119c04b8c67b5241cd7fcfed", size = 110058 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/d3/c67d7d49967344b51208ad19f105233be1afdf07d3dcb35b471900265227/tree_sitter_javascript-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6ca583dad4bd79d3053c310b9f7208cd597fd85f9947e4ab2294658bb5c11e35", size = 59333 }, + { url = "https://files.pythonhosted.org/packages/a5/db/ea0ee1547679d1750e80a0c4bc60b3520b166eeaf048764cfdd1ba3fd5e5/tree_sitter_javascript-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:94100e491a6a247aa4d14caf61230c171b6376c863039b6d9cd71255c2d815ec", size = 61071 }, + { url = "https://files.pythonhosted.org/packages/67/6e/07c4857e08be37bfb55bfb269863df8ec908b2f6a3f1893cd852b893ecab/tree_sitter_javascript-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6bc1055b061c5055ec58f39ee9b2e9efb8e6e0ae970838af74da0afb811f0a", size = 96999 }, + { url = "https://files.pythonhosted.org/packages/5f/f5/4de730afe8b9422845bc2064020a8a8f49ebd1695c04261c38d1b3e3edec/tree_sitter_javascript-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:056dc04fb6b24293f8c5fec43c14e7e16ba2075b3009c643abf8c85edc4c7c3c", size = 94020 }, + { url = "https://files.pythonhosted.org/packages/77/0a/f980520da86c4eff8392867840a945578ef43372c9d4a37922baa6b121fe/tree_sitter_javascript-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a11ca1c0f736da42967586b568dff8a465ee148a986c15ebdc9382806e0ce871", size = 92927 }, + { url = "https://files.pythonhosted.org/packages/ff/5c/36a98d512aa1d1082409d6b7eda5d26b820bd4477a54100ad9f62212bc55/tree_sitter_javascript-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:041fa22b34250ea6eb313d33104d5303f79504cb259d374d691e38bbdc49145b", size = 58824 }, + { url = "https://files.pythonhosted.org/packages/dc/79/ceb21988e6de615355a63eebcf806cd2a0fe875bec27b429d58b63e7fb5f/tree_sitter_javascript-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:eb28130cd2fb30d702d614cbf61ef44d1c7f6869e7d864a9cc17111e370be8f7", size = 57027 }, +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677 }, + { url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008 }, + { url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987 }, + { url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960 }, + { url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245 }, + { url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015 }, + { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052 }, +] + +[[package]] +name = "tree-sitter-zig" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/97/75967b81460e0ce999de4736b9ac189dcd5ad1c85aabcc398ba529f4838e/tree_sitter_zig-1.1.2.tar.gz", hash = "sha256:da24db16df92f7fcfa34448e06a14b637b1ff985f7ce2ee19183c489e187a92e", size = 194084 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/c6/db41d3f6c7c0174db56d9122a2a4d8b345c377ca87268e76557b2879675e/tree_sitter_zig-1.1.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e7542354a5edba377b5692b2add4f346501306d455e192974b7e76bf1a61a282", size = 61900 }, + { url = "https://files.pythonhosted.org/packages/5a/78/93d32fea98b3b031bc0fbec44e27f2b8cc1a1a8ff5a99dfb1a8f85b11d43/tree_sitter_zig-1.1.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:daa2cdd7c1a2d278f2a917c85993adb6e84d37778bfc350ee9e342872e7f8be2", size = 67837 }, + { url = "https://files.pythonhosted.org/packages/40/45/ef5afd6b79bd58731dae2cf61ff7960dd616737397db4d2e926457ff24b7/tree_sitter_zig-1.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1962e95067ac5ee784daddd573f828ef32f15e9c871967df6833d3d389113eae", size = 83391 }, + { url = "https://files.pythonhosted.org/packages/78/02/275523eb05108d83e154f52c7255763bac8b588ae14163563e19479322a7/tree_sitter_zig-1.1.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e924509dcac5a6054da357e3d6bcf37ea82984ee1d2a376569753d32f61ea8bb", size = 82323 }, + { url = "https://files.pythonhosted.org/packages/ef/e9/ff3c11097e37d4d899155c8fbdf7531063b6d15ee252b2e01ce0063f0218/tree_sitter_zig-1.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d8f463c370cdd71025b8d40f90e21e8fc25c7394eb64ebd53b1e566d712a3a68", size = 81383 }, + { url = "https://files.pythonhosted.org/packages/ab/5c/f5fb2ce355bbd381e647b04e8b2078a4043e663b6df6145d87550d3c3fe5/tree_sitter_zig-1.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:7b94f00a0e69231ac4ebf0aa763734b9b5637e0ff13634ebfe6d13fadece71e9", size = 65105 }, + { url = "https://files.pythonhosted.org/packages/34/8d/c0a481cc7bba9d39c533dd3098463854b5d3c4e6134496d9d83cd1331e51/tree_sitter_zig-1.1.2-cp39-abi3-win_arm64.whl", hash = "sha256:88152ebeaeca1431a6fc943a8b391fee6f6a8058f17435015135157735061ddf", size = 63219 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +]