diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..54ab2051 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,22 @@ +{ + "mcpServers": { + "test-mcp-server": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "D:/Claude_dms3" + ] + }, + "ccw-tools": { + "command": "npx", + "args": [ + "-y", + "ccw-mcp" + ], + "env": { + "CCW_ENABLED_TOOLS": "write_file,edit_file,codex_lens,smart_search" + } + } + } +} diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..93dc35af --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,190 @@ +# Implementation Summary: Rules CLI Generation Feature + +## Status: ✅ Complete + +## Files Modified + +### D:\Claude_dms3\ccw\src\core\routes\rules-routes.ts + +**Changes:** +1. Added import for `executeCliTool` from cli-executor +2. Implemented `generateRuleViaCLI()` function +3. Modified POST `/api/rules/create` endpoint to support `mode: 'cli-generate'` + +## Implementation Details + +### 1. New Function: `generateRuleViaCLI()` + +**Location:** lines 224-340 + +**Purpose:** Generate rule content using Gemini CLI based on different generation strategies + +**Parameters:** +- `generationType`: 'description' | 'template' | 'extract' +- `description`: Natural language description of the rule +- `templateType`: Template category for structured generation +- `extractScope`: File pattern for code analysis (e.g., 'src/**/*.ts') +- `extractFocus`: Focus areas for extraction (e.g., 'error handling, naming') +- `fileName`: Target filename (must end with .md) +- `location`: 'project' or 'user' +- `subdirectory`: Optional subdirectory path +- `projectPath`: Project root directory + +**Process Flow:** +1. Parse parameters and determine generation type +2. Build appropriate CLI prompt template based on type +3. Execute Gemini CLI with: + - Tool: 'gemini' + - Mode: 'write' for description/template, 'analysis' for extract + - Timeout: 10 minutes (600000ms) + - Working directory: projectPath +4. Validate CLI execution result +5. Extract generated content from stdout +6. Call `createRule()` to save the file +7. Return result with execution ID + +### 2. Prompt Templates + +#### Description Mode (write) +``` +PURPOSE: Generate Claude Code memory rule from description to guide Claude's behavior +TASK: • Analyze rule requirements • Generate markdown content with clear instructions +MODE: write +EXPECTED: Complete rule content in markdown format +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) +``` + +#### Template Mode (write) +``` +PURPOSE: Generate Claude Code rule from template type +TASK: • Create rule based on {templateType} • Generate structured markdown content +MODE: write +EXPECTED: Complete rule content in markdown format following template structure +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) +``` + +#### Extract Mode (analysis) +``` +PURPOSE: Extract coding rules from existing codebase to document patterns and conventions +TASK: • Analyze code patterns • Extract common conventions • Identify best practices +MODE: analysis +CONTEXT: @{extractScope || '**/*'} +EXPECTED: Rule content based on codebase analysis with examples +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/analysis/02-analyze-code-patterns.txt) +``` + +### 3. API Endpoint Modification + +**Endpoint:** POST `/api/rules/create` + +**Enhanced Request Body:** +```json +{ + "mode": "cli-generate", // NEW: triggers CLI generation + "generationType": "description", // NEW: 'description' | 'template' | 'extract' + "description": "...", // NEW: for description mode + "templateType": "...", // NEW: for template mode + "extractScope": "src/**/*.ts", // NEW: for extract mode + "extractFocus": "...", // NEW: for extract mode + "fileName": "rule-name.md", // REQUIRED + "location": "project", // REQUIRED: 'project' | 'user' + "subdirectory": "", // OPTIONAL + "projectPath": "..." // OPTIONAL: defaults to initialPath +} +``` + +**Backward Compatibility:** Existing manual creation still works: +```json +{ + "fileName": "rule-name.md", + "content": "# Rule Content\n...", + "location": "project", + "paths": [], + "subdirectory": "" +} +``` + +**Response Format:** +```json +{ + "success": true, + "fileName": "rule-name.md", + "location": "project", + "path": "/absolute/path/to/rule-name.md", + "subdirectory": null, + "generatedContent": "# Generated Content\n...", + "executionId": "1734168000000-gemini" +} +``` + +## Error Handling + +### Validation Errors +- Missing `fileName`: "File name is required" +- Missing `location`: "Location is required (project or user)" +- Missing `generationType` in CLI mode: "generationType is required for CLI generation mode" +- Missing `description` for description mode: "description is required for description-based generation" +- Missing `templateType` for template mode: "templateType is required for template-based generation" +- Unknown `generationType`: "Unknown generation type: {type}" + +### CLI Execution Errors +- CLI tool failure: Returns `{ error: "CLI execution failed: ...", stderr: "..." }` +- Empty content: Returns `{ error: "CLI execution returned empty content", stdout: "...", stderr: "..." }` +- Timeout: CLI executor will timeout after 10 minutes +- File exists: "Rule '{fileName}' already exists in {location} location" + +## Testing + +### Test Document +Created: `D:\Claude_dms3\test-rules-cli-generation.md` + +Contains: +- API usage examples for all 3 generation types +- Request/response format examples +- Error handling scenarios +- Integration details + +### Compilation Test +✅ TypeScript compilation successful (`npm run build`) + +## Integration Points + +### Dependencies +- **cli-executor.ts**: Provides `executeCliTool()` for Gemini execution +- **createRule()**: Existing function for file creation +- **handlePostRequest()**: Existing request handler from RouteContext + +### CLI Tool +- **Tool**: Gemini (via `executeCliTool()`) +- **Timeout**: 10 minutes (600000ms) +- **Mode**: 'write' for generation, 'analysis' for extraction +- **Working Directory**: Project path for context access + +## Next Steps (Not Implemented) + +1. **UI Integration**: Add frontend interface in Rules Manager dashboard +2. **Streaming Output**: Display CLI execution progress in real-time +3. **Preview**: Show generated content before saving +4. **Refinement**: Allow iterative refinement of generated rules +5. **Templates Library**: Add predefined template types +6. **History**: Track generation history and allow regeneration + +## Verification Checklist + +- [x] Import cli-executor functions +- [x] Implement `generateRuleViaCLI()` with 3 generation types +- [x] Build appropriate prompts for each type +- [x] Use correct MODE (analysis vs write) +- [x] Set timeout to at least 10 minutes +- [x] Integrate with `createRule()` for file creation +- [x] Modify POST endpoint to support `mode: 'cli-generate'` +- [x] Validate required parameters +- [x] Return unified result format +- [x] Handle errors appropriately +- [x] Maintain backward compatibility +- [x] Verify TypeScript compilation +- [x] Create test documentation + +## Files Created +- `D:\Claude_dms3\test-rules-cli-generation.md`: Test documentation +- `D:\Claude_dms3\IMPLEMENTATION_SUMMARY.md`: This file diff --git a/MCP_OPTIMIZATION_SUMMARY.md b/MCP_OPTIMIZATION_SUMMARY.md new file mode 100644 index 00000000..0ac84ad6 --- /dev/null +++ b/MCP_OPTIMIZATION_SUMMARY.md @@ -0,0 +1,202 @@ +# MCP 优化和模板功能实现总结 + +## 完成的功能 + +### 1. ✅ .mcp.json 支持优化 + +#### 后端改进(`mcp-routes.ts`) +- **新增函数**: + - `addMcpServerToMcpJson()` - 添加服务器到 .mcp.json + - `removeMcpServerFromMcpJson()` - 从 .mcp.json 删除服务器 + +- **优化的配置优先级**: + ``` + 1. Enterprise managed-mcp.json (最高优先级,不可覆盖) + 2. .mcp.json (项目级,新默认) ← 优先级提升 + 3. ~/.claude.json projects[path].mcpServers (遗留支持) + 4. ~/.claude.json mcpServers (用户全局) + ``` + +- **智能安装逻辑**: + - `addMcpServerToProject()` 默认写入 `.mcp.json` + - 仍然支持 `.claude.json`(向后兼容) + - `removeMcpServerFromProject()` 自动检测并从两处删除 + +- **元数据跟踪**: + ```json + { + "mcpJsonPath": "D:\\Claude_dms3\\.mcp.json", + "hasMcpJson": true + } + ``` + +#### 前端 UI 改进(`mcp-manager.js`) +- **配置来源指示器**: + - 有 .mcp.json:显示绿色 `file-check` 图标 + - 无 .mcp.json:显示提示 "Will use .mcp.json" + +- **项目概览表增强**: + - 每个项目旁显示 `.mcp.json` 状态图标 + - 清晰区分配置来源 + +### 2. ✅ MCP 模板系统 + +#### 数据库模块(`mcp-templates-db.ts`) +- **数据库位置**:`~/.ccw/mcp-templates.db` +- **模板结构**: + ```typescript + interface McpTemplate { + id?: number; + name: string; + description?: string; + serverConfig: { + command: string; + args?: string[]; + env?: Record; + }; + tags?: string[]; + category?: string; + createdAt?: number; + updatedAt?: number; + } + ``` + +- **功能**: + - `saveTemplate()` - 保存/更新模板 + - `getAllTemplates()` - 获取所有模板 + - `getTemplateByName()` - 按名称查找 + - `getTemplatesByCategory()` - 按分类查找 + - `searchTemplates()` - 关键字搜索 + - `deleteTemplate()` - 删除模板 + +#### API 端点 +| 方法 | 路径 | 功能 | +|------|------|------| +| GET | `/api/mcp-templates` | 获取所有模板 | +| POST | `/api/mcp-templates` | 保存模板 | +| GET | `/api/mcp-templates/:name` | 获取单个模板 | +| DELETE | `/api/mcp-templates/:name` | 删除模板 | +| GET | `/api/mcp-templates/search?q=keyword` | 搜索模板 | +| GET | `/api/mcp-templates/categories` | 获取所有分类 | +| GET | `/api/mcp-templates/category/:name` | 按分类获取 | +| POST | `/api/mcp-templates/install` | 安装模板到项目/全局 | + +### 3. ✅ Bug 修复 + +#### 删除服务器逻辑优化 +- **问题**:无法正确删除来自 .mcp.json 的服务器 +- **解决**: + ```typescript + // 现在会同时检查两个位置 + removeMcpServerFromProject() { + // 尝试从 .mcp.json 删除 + // 也尝试从 .claude.json 删除 + // 返回详细的删除结果 + } + ``` + +## 测试验证 + +### 1. .mcp.json 识别测试 +```bash +$ curl http://localhost:3456/api/mcp-config | jq +``` +✅ 成功识别 `D:\Claude_dms3\.mcp.json` +✅ 正确加载服务器配置: + - test-mcp-server + - ccw-tools (含环境变量) + +### 2. 创建的测试文件 +- `D:\Claude_dms3\.mcp.json` - 测试配置文件 + +## 待实现功能 + +### 前端 UI(下一步) +- [ ] 模板管理界面 + - 模板列表视图 + - 创建/编辑模板表单 + - 模板预览 + - 从现有服务器保存为模板 + - 从模板快速安装 + +### CCW Tools 安装增强 +- [ ] 全局安装选项 + - 添加到 ~/.claude.json + - 所有项目可用 + +- [ ] 项目安装选项(当前默认) + - 写入 .mcp.json + - 仅当前项目可用 + +## 使用示例 + +### 保存当前服务器为模板 +```javascript +// POST /api/mcp-templates +{ + "name": "filesystem-server", + "description": "MCP Filesystem server for local files", + "serverConfig": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"] + }, + "category": "官方", + "tags": ["filesystem", "mcp", "official"] +} +``` + +### 安装模板到项目 +```javascript +// POST /api/mcp-templates/install +{ + "templateName": "filesystem-server", + "projectPath": "D:/Claude_dms3", + "scope": "project" // 或 "global" +} +``` + +## 架构优势 + +### 1. 清晰的配置层次 +- **企业级** → 组织统一管理 +- **.mcp.json** → 项目团队共享(可提交 git) +- **.claude.json** → 用户个人配置(不提交) + +### 2. 向后兼容 +- 遗留 `.claude.json` 配置仍然有效 +- 平滑迁移路径 + +### 3. 模板复用 +- 常用配置保存为模板 +- 跨项目快速部署 +- 团队共享最佳实践 + +## 文件修改清单 + +### 新增文件 +1. `ccw/src/core/mcp-templates-db.ts` - 模板数据库模块 + +### 修改文件 +1. `ccw/src/core/routes/mcp-routes.ts` + - 添加 .mcp.json 读写函数 + - 优化配置优先级 + - 添加模板 API 路由 + - 修复删除逻辑 + +2. `ccw/src/templates/dashboard-js/views/mcp-manager.js` + - 添加 .mcp.json 状态显示 + - 项目概览表增强 + +### 测试文件 +1. `D:\Claude_dms3\.mcp.json` - 测试配置 + +## 下一步计划 + +1. **完成前端模板管理 UI** +2. **实现 CCW Tools 全局/项目安装切换** +3. **添加预设模板库**(官方 MCP 服务器) +4. **模板导入/导出功能** + +--- +生成时间:2025-12-14 +Claude Code Workflow v6.1.4 diff --git a/ccw/src/core/mcp-templates-db.ts b/ccw/src/core/mcp-templates-db.ts new file mode 100644 index 00000000..97a125bb --- /dev/null +++ b/ccw/src/core/mcp-templates-db.ts @@ -0,0 +1,269 @@ +// @ts-nocheck +/** + * MCP Templates Database Module + * Stores MCP server configurations as reusable templates + */ +import Database from 'better-sqlite3'; +import { existsSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { homedir } from 'os'; + +// Database path +const DB_DIR = join(homedir(), '.ccw'); +const DB_PATH = join(DB_DIR, 'mcp-templates.db'); + +// Ensure database directory exists +if (!existsSync(DB_DIR)) { + mkdirSync(DB_DIR, { recursive: true }); +} + +// Initialize database connection +let db: Database.Database | null = null; + +/** + * Get or create database connection + */ +function getDb(): Database.Database { + if (!db) { + db = new Database(DB_PATH); + initDatabase(); + } + return db; +} + +/** + * Initialize database schema + */ +function initDatabase() { + const db = getDb(); + + // Create templates table + db.exec(` + CREATE TABLE IF NOT EXISTS mcp_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + server_config TEXT NOT NULL, + tags TEXT, + category TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + `); + + // Create index on name for fast lookups + db.exec(` + CREATE INDEX IF NOT EXISTS idx_mcp_templates_name + ON mcp_templates(name) + `); + + // Create index on category for filtering + db.exec(` + CREATE INDEX IF NOT EXISTS idx_mcp_templates_category + ON mcp_templates(category) + `); +} + +export interface McpTemplate { + id?: number; + name: string; + description?: string; + serverConfig: { + command: string; + args?: string[]; + env?: Record; + }; + tags?: string[]; + category?: string; + createdAt?: number; + updatedAt?: number; +} + +/** + * Save MCP template to database + */ +export function saveTemplate(template: McpTemplate): { success: boolean; id?: number; error?: string } { + try { + const db = getDb(); + const now = Date.now(); + + const stmt = db.prepare(` + INSERT INTO mcp_templates (name, description, server_config, tags, category, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(name) DO UPDATE SET + description = excluded.description, + server_config = excluded.server_config, + tags = excluded.tags, + category = excluded.category, + updated_at = excluded.updated_at + `); + + const result = stmt.run( + template.name, + template.description || null, + JSON.stringify(template.serverConfig), + template.tags ? JSON.stringify(template.tags) : null, + template.category || null, + template.createdAt || now, + now + ); + + return { + success: true, + id: result.lastInsertRowid as number + }; + } catch (error: unknown) { + console.error('Error saving MCP template:', error); + return { + success: false, + error: (error as Error).message + }; + } +} + +/** + * Get all MCP templates + */ +export function getAllTemplates(): McpTemplate[] { + try { + const db = getDb(); + const rows = db.prepare('SELECT * FROM mcp_templates ORDER BY name').all(); + + return rows.map((row: any) => ({ + id: row.id, + name: row.name, + description: row.description, + serverConfig: JSON.parse(row.server_config), + tags: row.tags ? JSON.parse(row.tags) : [], + category: row.category, + createdAt: row.created_at, + updatedAt: row.updated_at + })); + } catch (error: unknown) { + console.error('Error getting MCP templates:', error); + return []; + } +} + +/** + * Get template by name + */ +export function getTemplateByName(name: string): McpTemplate | null { + try { + const db = getDb(); + const row = db.prepare('SELECT * FROM mcp_templates WHERE name = ?').get(name); + + if (!row) return null; + + return { + id: (row as any).id, + name: (row as any).name, + description: (row as any).description, + serverConfig: JSON.parse((row as any).server_config), + tags: (row as any).tags ? JSON.parse((row as any).tags) : [], + category: (row as any).category, + createdAt: (row as any).created_at, + updatedAt: (row as any).updated_at + }; + } catch (error: unknown) { + console.error('Error getting MCP template:', error); + return null; + } +} + +/** + * Get templates by category + */ +export function getTemplatesByCategory(category: string): McpTemplate[] { + try { + const db = getDb(); + const rows = db.prepare('SELECT * FROM mcp_templates WHERE category = ? ORDER BY name').all(category); + + return rows.map((row: any) => ({ + id: row.id, + name: row.name, + description: row.description, + serverConfig: JSON.parse(row.server_config), + tags: row.tags ? JSON.parse(row.tags) : [], + category: row.category, + createdAt: row.created_at, + updatedAt: row.updated_at + })); + } catch (error: unknown) { + console.error('Error getting MCP templates by category:', error); + return []; + } +} + +/** + * Delete template by name + */ +export function deleteTemplate(name: string): { success: boolean; error?: string } { + try { + const db = getDb(); + const result = db.prepare('DELETE FROM mcp_templates WHERE name = ?').run(name); + + return { + success: result.changes > 0 + }; + } catch (error: unknown) { + console.error('Error deleting MCP template:', error); + return { + success: false, + error: (error as Error).message + }; + } +} + +/** + * Search templates by keyword + */ +export function searchTemplates(keyword: string): McpTemplate[] { + try { + const db = getDb(); + const searchPattern = `%${keyword}%`; + const rows = db.prepare(` + SELECT * FROM mcp_templates + WHERE name LIKE ? OR description LIKE ? OR tags LIKE ? + ORDER BY name + `).all(searchPattern, searchPattern, searchPattern); + + return rows.map((row: any) => ({ + id: row.id, + name: row.name, + description: row.description, + serverConfig: JSON.parse(row.server_config), + tags: row.tags ? JSON.parse(row.tags) : [], + category: row.category, + createdAt: row.created_at, + updatedAt: row.updated_at + })); + } catch (error: unknown) { + console.error('Error searching MCP templates:', error); + return []; + } +} + +/** + * Get all categories + */ +export function getAllCategories(): string[] { + try { + const db = getDb(); + const rows = db.prepare('SELECT DISTINCT category FROM mcp_templates WHERE category IS NOT NULL ORDER BY category').all(); + return rows.map((row: any) => row.category); + } catch (error: unknown) { + console.error('Error getting categories:', error); + return []; + } +} + +/** + * Close database connection + */ +export function closeDb() { + if (db) { + db.close(); + db = null; + } +} diff --git a/ccw/src/core/routes/claude-routes.ts b/ccw/src/core/routes/claude-routes.ts new file mode 100644 index 00000000..31de9139 --- /dev/null +++ b/ccw/src/core/routes/claude-routes.ts @@ -0,0 +1,804 @@ +// @ts-nocheck +/** + * CLAUDE.md Routes Module + * Handles all CLAUDE.md memory rules management endpoints + */ +import type { IncomingMessage, ServerResponse } from 'http'; +import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs'; +import { join, relative } from 'path'; +import { homedir } from 'os'; + +export interface RouteContext { + pathname: string; + url: URL; + req: IncomingMessage; + res: ServerResponse; + initialPath: string; + handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise) => void; + broadcastToClients: (data: unknown) => void; +} + +interface ClaudeFile { + id: string; + level: 'user' | 'project' | 'module'; + path: string; + relativePath: string; + name: string; + content?: string; + size: number; + lastModified: string; + frontmatter?: { paths?: string[] }; + stats?: { lines: number; words: number; characters: number }; + isMainFile: boolean; + parentDirectory?: string; + depth?: number; +} + +interface ClaudeFilesHierarchy { + user: { main: ClaudeFile | null }; + project: { main: ClaudeFile | null }; + modules: ClaudeFile[]; + summary: { totalFiles: number; totalSize: number; lastSync?: string }; +} + +/** + * Parse frontmatter from markdown file + * Reuses logic from rules-routes.ts + */ +function parseClaudeFrontmatter(content: string) { + const result = { + paths: [] as string[], + content: content + }; + + if (content.startsWith('---')) { + const endIndex = content.indexOf('---', 3); + if (endIndex > 0) { + const frontmatter = content.substring(3, endIndex).trim(); + result.content = content.substring(endIndex + 3).trim(); + + const lines = frontmatter.split('\n'); + for (const line of lines) { + const colonIndex = line.indexOf(':'); + if (colonIndex > 0) { + const key = line.substring(0, colonIndex).trim().toLowerCase(); + const value = line.substring(colonIndex + 1).trim(); + + if (key === 'paths') { + result.paths = value.replace(/^\[|\]$/g, '').split(',').map(t => t.trim()).filter(Boolean); + } + } + } + } + } + + return result; +} + +/** + * Calculate file statistics + */ +function calculateFileStats(content: string) { + const lines = content.split('\n').length; + const words = content.split(/\s+/).filter(w => w.length > 0).length; + const characters = content.length; + return { lines, words, characters }; +} + +/** + * Create ClaudeFile object from file path + */ +function createClaudeFile( + filePath: string, + level: 'user' | 'project' | 'module', + basePath: string, + isMainFile: boolean, + depth?: number +): ClaudeFile | null { + try { + if (!existsSync(filePath)) return null; + + const stat = statSync(filePath); + const content = readFileSync(filePath, 'utf8'); + const parsed = parseClaudeFrontmatter(content); + const relativePath = relative(basePath, filePath).replace(/\\/g, '/'); + const fileName = filePath.split(/[\\/]/).pop() || 'CLAUDE.md'; + + // Parent directory for module-level files + const parentDir = level === 'module' + ? filePath.split(/[\\/]/).slice(-2, -1)[0] + : undefined; + + return { + id: `${level}-${relativePath}`, + level, + path: filePath, + relativePath, + name: fileName, + content: parsed.content, + size: stat.size, + lastModified: stat.mtime.toISOString(), + frontmatter: { paths: parsed.paths }, + stats: calculateFileStats(content), + isMainFile, + parentDirectory: parentDir, + depth + }; + } catch (e) { + console.error(`Error creating ClaudeFile for ${filePath}:`, e); + return null; + } +} + +/** + * Scan rules directory (recursive) + * Adapted from rules-routes.ts::scanRulesDirectory + */ +function scanClaudeRulesDirectory(dirPath: string, level: 'user' | 'project', basePath: string): ClaudeFile[] { + const files: ClaudeFile[] = []; + + try { + const entries = readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dirPath, entry.name); + + if (entry.isFile() && entry.name.endsWith('.md')) { + const file = createClaudeFile(fullPath, level, basePath, false); + if (file) files.push(file); + } else if (entry.isDirectory()) { + const subFiles = scanClaudeRulesDirectory(fullPath, level, basePath); + files.push(...subFiles); + } + } + } catch (e) { + // Ignore errors + } + + return files; +} + +/** + * Scan modules for CLAUDE.md files + * Uses get-modules-by-depth logic + */ +function scanModules(projectPath: string): ClaudeFile[] { + const modules: ClaudeFile[] = []; + const visited = new Set(); + + // Directories to exclude (from get-modules-by-depth.ts) + const SYSTEM_EXCLUDES = [ + '.git', '.svn', '.hg', '__pycache__', 'node_modules', '.npm', '.yarn', + 'dist', 'build', 'out', '.cache', '.venv', 'venv', 'env', 'coverage' + ]; + + function scanDirectory(dirPath: string, depth: number) { + try { + const entries = readdirSync(dirPath, { withFileTypes: true }); + + // Check for CLAUDE.md in current directory + const claudePath = join(dirPath, 'CLAUDE.md'); + if (existsSync(claudePath) && !visited.has(claudePath)) { + visited.add(claudePath); + const file = createClaudeFile(claudePath, 'module', projectPath, true, depth); + if (file) modules.push(file); + } + + // Recurse into subdirectories + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (SYSTEM_EXCLUDES.includes(entry.name)) continue; + + const fullPath = join(dirPath, entry.name); + scanDirectory(fullPath, depth + 1); + } + } catch (e) { + // Ignore permission errors + } + } + + scanDirectory(projectPath, 0); + return modules.sort((a, b) => (b.depth || 0) - (a.depth || 0)); // Deepest first +} + +/** + * Scan all CLAUDE.md files + */ +function scanAllClaudeFiles(projectPath: string): ClaudeFilesHierarchy { + const result: ClaudeFilesHierarchy = { + user: { main: null }, + project: { main: null }, + modules: [], + summary: { totalFiles: 0, totalSize: 0 } + }; + + // User-level files (only main CLAUDE.md, no rules) + const userHome = homedir(); + const userClaudeDir = join(userHome, '.claude'); + const userClaudePath = join(userClaudeDir, 'CLAUDE.md'); + + if (existsSync(userClaudePath)) { + result.user.main = createClaudeFile(userClaudePath, 'user', userHome, true); + } + + // Project-level files (only main CLAUDE.md, no rules) + const projectClaudeDir = join(projectPath, '.claude'); + const projectClaudePath = join(projectClaudeDir, 'CLAUDE.md'); + + if (existsSync(projectClaudePath)) { + result.project.main = createClaudeFile(projectClaudePath, 'project', projectPath, true); + } + + // Module-level files + result.modules = scanModules(projectPath); + + // Calculate summary (only main CLAUDE.md files, no rules) + const allFiles = [ + result.user.main, + result.project.main, + ...result.modules + ].filter(f => f !== null) as ClaudeFile[]; + + result.summary = { + totalFiles: allFiles.length, + totalSize: allFiles.reduce((sum, f) => sum + f.size, 0), + lastSync: new Date().toISOString() + }; + + return result; +} + +/** + * Get single file content + */ +function getClaudeFile(filePath: string): ClaudeFile | null { + try { + if (!existsSync(filePath)) { + return null; + } + + const stat = statSync(filePath); + const content = readFileSync(filePath, 'utf8'); + const parsed = parseClaudeFrontmatter(content); + + // Determine level based on path + let level: 'user' | 'project' | 'module' = 'module'; + if (filePath.includes(join(homedir(), '.claude'))) { + level = 'user'; + } else if (filePath.includes('.claude')) { + level = 'project'; + } + + const isMainFile = filePath.endsWith('CLAUDE.md') && !filePath.includes('rules'); + + return { + id: `${level}-${filePath}`, + level, + path: filePath, + relativePath: filePath, + name: filePath.split(/[\\/]/).pop() || 'CLAUDE.md', + content: parsed.content, + size: stat.size, + lastModified: stat.mtime.toISOString(), + frontmatter: { paths: parsed.paths }, + stats: calculateFileStats(content), + isMainFile + }; + } catch (error) { + console.error('Error reading CLAUDE.md file:', error); + return null; + } +} + +/** + * Save file content + */ +function saveClaudeFile(filePath: string, content: string, createBackup: boolean = false): { success: boolean; error?: string } { + try { + if (!existsSync(filePath)) { + return { success: false, error: 'File not found' }; + } + + // Create backup if requested + if (createBackup) { + const backupPath = `${filePath}.backup-${Date.now()}`; + const originalContent = readFileSync(filePath, 'utf8'); + writeFileSync(backupPath, originalContent, 'utf8'); + } + + // Write new content + writeFileSync(filePath, content, 'utf8'); + + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } +} + +/** + * Generate CLI prompt for syncing CLAUDE.md files + */ +function generateSyncPrompt(level: 'user' | 'project' | 'module', modulePath?: string): string { + if (level === 'module' && modulePath) { + // Module-level prompt + return `PURPOSE: Generate module-level CLAUDE.md for ${modulePath} +TASK: • Analyze module's purpose and responsibilities • Document public APIs and interfaces • Identify dependencies and integration points • Note testing patterns and conventions +MODE: analysis +CONTEXT: @${modulePath}/**/* | Memory: Project conventions from .claude/CLAUDE.md +EXPECTED: Module-level CLAUDE.md with: - Module purpose (1-2 sentences) - Key files and their roles - Public API documentation - Integration points - Testing approach +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/planning/02-design-component-spec.txt) | Module-level perspective only | Concrete examples | analysis=READ-ONLY`; + } else { + // User/Project level prompt + const contextPath = level === 'user' ? '~/.claude' : '.claude'; + return `PURPOSE: Update CLAUDE.md with current ${level} understanding +TASK: • Analyze ${level} configuration and conventions • Identify common patterns and anti-patterns • Generate concise, actionable rules • Maintain existing structure and formatting +MODE: analysis +CONTEXT: @${contextPath}/**/* +EXPECTED: Updated CLAUDE.md content with: - Preserved existing sections - New insights appended to relevant sections - Timestamp header - Focus on ${level}-level concerns +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/analysis/02-analyze-code-patterns.txt) | Maintain existing CLAUDE.md structure | Focus on actionable rules | analysis=READ-ONLY`; + } +} + +/** + * Smart merge CLAUDE.md content (update mode) + */ +function smartMergeContent(existingContent: string, cliOutput: string): string { + // For now, use simple append strategy + // TODO: Implement intelligent section-based merging + const timestamp = new Date().toISOString(); + const separator = '\n\n---\n\n'; + const header = `## Updated: ${timestamp}\n\n`; + + return existingContent + separator + header + cliOutput; +} + +/** + * Scan all files in project directory + */ +function scanAllProjectFiles(projectPath: string): any { + const SYSTEM_EXCLUDES = [ + '.git', '.svn', '.hg', '__pycache__', 'node_modules', '.npm', '.yarn', + 'dist', 'build', 'out', '.cache', '.venv', 'venv', 'env', 'coverage', + '.next', '.nuxt', '.output', '.turbo', '.parcel-cache', 'logs', 'tmp', 'temp' + ]; + + const results: any = { + files: [], + summary: { totalFiles: 0, totalDirectories: 0, totalSize: 0 } + }; + + function scanDir(dirPath: string, depth: number = 0): any[] { + if (depth > 10) return []; // Max depth limit + + const files: any[] = []; + + try { + const entries = readdirSync(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + // Skip system excludes and hidden files (except .claude and .workflow) + if (SYSTEM_EXCLUDES.includes(entry.name)) continue; + if (entry.name.startsWith('.') && entry.name !== '.claude' && entry.name !== '.workflow') continue; + + const fullPath = join(dirPath, entry.name); + const relativePath = relative(projectPath, fullPath).replace(/\\/g, '/'); + + if (entry.isDirectory()) { + results.summary.totalDirectories++; + + const dirNode: any = { + path: fullPath, + name: entry.name, + type: 'directory', + depth, + children: scanDir(fullPath, depth + 1) + }; + + files.push(dirNode); + } else { + const stat = statSync(fullPath); + results.summary.totalFiles++; + results.summary.totalSize += stat.size; + + files.push({ + path: fullPath, + name: entry.name, + type: 'file', + size: stat.size, + lastModified: stat.mtime.toISOString(), + depth + }); + } + } + } catch (e) { + // Ignore permission errors + } + + return files; + } + + results.files = scanDir(projectPath); + return results; +} + +/** + * Read single file content + */ +function readSingleFile(filePath: string): { content: string; size: number; lastModified: string } | null { + try { + if (!existsSync(filePath)) return null; + const stat = statSync(filePath); + const content = readFileSync(filePath, 'utf8'); + return { + content, + size: stat.size, + lastModified: stat.mtime.toISOString() + }; + } catch (e) { + return null; + } +} + +/** + * Delete CLAUDE.md file + */ +function deleteClaudeFile(filePath: string): { success: boolean; error?: string } { + try { + if (!existsSync(filePath)) { + return { success: false, error: 'File not found' }; + } + + // Create backup before deletion + const backupPath = `${filePath}.deleted-${Date.now()}`; + const content = readFileSync(filePath, 'utf8'); + writeFileSync(backupPath, content, 'utf8'); + + // Delete original file + const fs = require('fs'); + fs.unlinkSync(filePath); + + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } +} + +/** + * Create new CLAUDE.md file with template + */ +function createNewClaudeFile(level: 'user' | 'project' | 'module', template: string, pathParam?: string): { success: boolean; path?: string; error?: string } { + try { + let filePath: string; + let content: string; + + // Determine file path + if (level === 'user') { + filePath = join(homedir(), '.claude', 'CLAUDE.md'); + } else if (level === 'project' && pathParam) { + filePath = join(pathParam, '.claude', 'CLAUDE.md'); + } else if (level === 'module' && pathParam) { + filePath = join(pathParam, 'CLAUDE.md'); + } else { + return { success: false, error: 'Invalid parameters' }; + } + + // Check if file already exists + if (existsSync(filePath)) { + return { success: false, error: 'File already exists' }; + } + + // Generate content based on template + const timestamp = new Date().toISOString(); + + if (template === 'minimal') { + content = `# CLAUDE.md (${level.toUpperCase()} Level)\n\n> Created: ${timestamp}\n\n## Purpose\n\n[Describe the purpose of this ${level}-level context]\n\n## Guidelines\n\n- [Add guideline 1]\n- [Add guideline 2]\n`; + } else if (template === 'comprehensive') { + content = `# CLAUDE.md (${level.toUpperCase()} Level)\n\n> Created: ${timestamp}\n\n## Purpose\n\n[Describe the purpose and scope]\n\n## Architecture\n\n[Describe key architectural decisions]\n\n## Coding Conventions\n\n### Naming\n\n- [Convention 1]\n- [Convention 2]\n\n### Patterns\n\n- [Pattern 1]\n- [Pattern 2]\n\n## Testing Guidelines\n\n[Testing approach and conventions]\n\n## Dependencies\n\n[Key dependencies and integration points]\n\n## Common Tasks\n\n### Task 1\n\n[Steps for task 1]\n\n### Task 2\n\n[Steps for task 2]\n`; + } else { + // default template + content = `# CLAUDE.md (${level.toUpperCase()} Level)\n\n> Created: ${timestamp}\n\n## Overview\n\n[Brief description of this ${level}-level context]\n\n## Key Conventions\n\n- [Convention 1]\n- [Convention 2]\n- [Convention 3]\n\n## Guidelines\n\n### Code Style\n\n[Style guidelines]\n\n### Best Practices\n\n[Best practices]\n`; + } + + // Ensure directory exists + const dir = filePath.substring(0, filePath.lastIndexOf('/') || filePath.lastIndexOf('\\')); + const fs = require('fs'); + if (!existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // Write file + writeFileSync(filePath, content, 'utf8'); + + return { success: true, path: filePath }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } +} + +/** + * Handle CLAUDE.md routes + */ +export async function handleClaudeRoutes(ctx: RouteContext): Promise { + const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx; + + // API: Scan all CLAUDE.md files + if (pathname === '/api/memory/claude/scan') { + const projectPathParam = url.searchParams.get('path') || initialPath; + const filesData = scanAllClaudeFiles(projectPathParam); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(filesData)); + return true; + } + + // API: Scan all project files (not just CLAUDE.md) + if (pathname === '/api/memory/claude/scan-all') { + const projectPathParam = url.searchParams.get('path') || initialPath; + const filesData = scanAllProjectFiles(projectPathParam); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(filesData)); + return true; + } + + // API: Read single file + if (pathname === '/api/memory/claude/read-file' && req.method === 'GET') { + const filePath = url.searchParams.get('path'); + if (!filePath) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing path parameter' })); + return true; + } + + const fileData = readSingleFile(filePath); + if (!fileData) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'File not found' })); + return true; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(fileData)); + return true; + } + + // API: CLI Sync (analyze and update CLAUDE.md using CLI tools) + if (pathname === '/api/memory/claude/sync' && req.method === 'POST') { + handlePostRequest(req, res, async (body: any) => { + const { level, path: modulePath, tool = 'gemini', mode = 'update', targets } = body; + + if (!level) { + return { error: 'Missing level parameter', status: 400 }; + } + + try { + // Import CLI executor + const { executeCliTool } = await import('../../tools/cli-executor.js'); + + // Determine file path based on level + let filePath: string; + let workingDir: string; + + if (level === 'user') { + filePath = join(homedir(), '.claude', 'CLAUDE.md'); + workingDir = join(homedir(), '.claude'); + } else if (level === 'project') { + filePath = join(initialPath, '.claude', 'CLAUDE.md'); + workingDir = join(initialPath, '.claude'); + } else if (level === 'module' && modulePath) { + filePath = join(modulePath, 'CLAUDE.md'); + workingDir = modulePath; + } else { + return { error: 'Invalid level or missing path for module level', status: 400 }; + } + + // Check if file exists (for update/append modes) + const fileExists = existsSync(filePath); + if (!fileExists && mode !== 'generate') { + return { error: 'File does not exist. Use generate mode to create it.', status: 404 }; + } + + // Read existing content + const existingContent = fileExists ? readFileSync(filePath, 'utf8') : ''; + + // Generate CLI prompt + const cliPrompt = generateSyncPrompt(level, modulePath); + + // Execute CLI tool + const syncId = `claude-sync-${level}-${Date.now()}`; + const result = await executeCliTool({ + tool: tool === 'qwen' ? 'qwen' : 'gemini', + prompt: cliPrompt, + mode: 'analysis', + format: 'plain', + cd: workingDir, + timeout: 600000, // 10 minutes + stream: false, + category: 'internal', + id: syncId + }); + + if (!result.success || !result.execution?.output) { + return { + error: 'CLI execution failed', + details: result.execution?.error || 'No output received', + status: 500 + }; + } + + // Extract CLI output + const cliOutput = typeof result.execution.output === 'string' + ? result.execution.output + : result.execution.output.stdout || ''; + + if (!cliOutput || cliOutput.trim().length === 0) { + return { error: 'CLI returned empty output', status: 500 }; + } + + // Process content based on mode + let finalContent: string; + + if (mode === 'generate') { + // Full replace + const timestamp = new Date().toISOString(); + finalContent = `# CLAUDE.md (${level.toUpperCase()} Level)\n\n> Auto-generated using ${tool.toUpperCase()}\n> Last updated: ${timestamp}\n\n---\n\n${cliOutput}`; + } else if (mode === 'append') { + // Simple append + const timestamp = new Date().toISOString(); + finalContent = existingContent + `\n\n---\n\n## Updated: ${timestamp}\n\n${cliOutput}`; + } else { + // Smart merge (update mode) + finalContent = smartMergeContent(existingContent, cliOutput); + } + + // Write updated content + writeFileSync(filePath, finalContent, 'utf8'); + + // Broadcast WebSocket event + broadcastToClients({ + type: 'CLAUDE_FILE_SYNCED', + payload: { + path: filePath, + level, + tool, + mode, + executionId: syncId, + timestamp: new Date().toISOString() + } + }); + + return { + success: true, + path: filePath, + executionId: syncId, + mode, + tool + }; + + } catch (error) { + console.error('Error syncing CLAUDE.md file:', error); + return { + error: 'Sync failed', + details: (error as Error).message, + status: 500 + }; + } + }); + return true; + } + + // API: Get single file + if (pathname === '/api/memory/claude/file' && req.method === 'GET') { + const filePath = url.searchParams.get('path'); + if (!filePath) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing path parameter' })); + return true; + } + + const file = getClaudeFile(filePath); + if (!file) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'File not found' })); + return true; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(file)); + return true; + } + + // API: Save file + if (pathname === '/api/memory/claude/file' && req.method === 'POST') { + handlePostRequest(req, res, async (body: any) => { + const { path: filePath, content, createBackup } = body; + + if (!filePath || content === undefined) { + return { error: 'Missing path or content parameter', status: 400 }; + } + + const result = saveClaudeFile(filePath, content, createBackup); + + if (result.success) { + // Broadcast update to all clients + ctx.broadcastToClients({ + type: 'CLAUDE_FILE_UPDATED', + data: { path: filePath } + }); + return { success: true, path: filePath }; + } else { + return { error: result.error, status: 500 }; + } + }); + return true; + } + + // API: Delete file + if (pathname === '/api/memory/claude/file' && req.method === 'DELETE') { + const filePath = url.searchParams.get('path'); + const confirm = url.searchParams.get('confirm'); + + if (!filePath) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing path parameter' })); + return true; + } + + if (confirm !== 'true') { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Confirmation required' })); + return true; + } + + const result = deleteClaudeFile(filePath); + + if (result.success) { + broadcastToClients({ + type: 'CLAUDE_FILE_DELETED', + data: { path: filePath } + }); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true })); + } else { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: result.error })); + } + return true; + } + + // API: Create file + if (pathname === '/api/memory/claude/create' && req.method === 'POST') { + handlePostRequest(req, res, async (body: any) => { + const { level, path, template = 'default' } = body; + + if (!level) { + return { error: 'Missing level parameter', status: 400 }; + } + + let result: any; + + if (level === 'project') { + // For project level, use initialPath + const filePath = join(initialPath, '.claude', 'CLAUDE.md'); + result = createNewClaudeFile(level, template, initialPath); + } else if (level === 'module') { + if (!path) { + return { error: 'Module path required', status: 400 }; + } + result = createNewClaudeFile(level, template, path); + } else { + result = createNewClaudeFile(level, template); + } + + if (result.success) { + broadcastToClients({ + type: 'CLAUDE_FILE_CREATED', + data: { path: result.path, level } + }); + return { success: true, path: result.path }; + } else { + return { error: result.error, status: 500 }; + } + }); + return true; + } + + return false; +} diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index ff977282..4c995bbb 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -9,7 +9,8 @@ import { bootstrapVenv, executeCodexLens, checkSemanticStatus, - installSemantic + installSemantic, + uninstallCodexLens } from '../../tools/codex-lens.js'; export interface RouteContext { @@ -44,6 +45,11 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise const result = await bootstrapVenv(); if (result.success) { const status = await checkVenvStatus(); + // Broadcast installation event + broadcastToClients({ + type: 'CODEXLENS_INSTALLED', + payload: { version: status.version, timestamp: new Date().toISOString() } + }); return { success: true, message: 'CodexLens installed successfully', version: status.version }; } else { return { success: false, error: result.error, status: 500 }; @@ -55,6 +61,103 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise return true; } + // API: CodexLens Uninstall + if (pathname === '/api/codexlens/uninstall' && req.method === 'POST') { + handlePostRequest(req, res, async () => { + try { + const result = await uninstallCodexLens(); + if (result.success) { + // Broadcast uninstallation event + broadcastToClients({ + type: 'CODEXLENS_UNINSTALLED', + payload: { timestamp: new Date().toISOString() } + }); + return { success: true, message: 'CodexLens uninstalled successfully' }; + } else { + return { success: false, error: result.error, status: 500 }; + } + } catch (err) { + return { success: false, error: err.message, status: 500 }; + } + }); + return true; + } + + // API: CodexLens Config - GET (Get current configuration) + if (pathname === '/api/codexlens/config' && req.method === 'GET') { + try { + const result = await executeCodexLens(['config-show', '--json']); + if (result.success) { + try { + const config = JSON.parse(result.output); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(config)); + } catch { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ index_dir: '~/.codexlens/indexes', index_count: 0 })); + } + } else { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ index_dir: '~/.codexlens/indexes', index_count: 0 })); + } + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + } + return true; + } + + // API: CodexLens Config - POST (Set configuration) + if (pathname === '/api/codexlens/config' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { index_dir } = body; + + if (!index_dir) { + return { success: false, error: 'index_dir is required', status: 400 }; + } + + try { + const result = await executeCodexLens(['config-set', '--key', 'index_dir', '--value', index_dir, '--json']); + if (result.success) { + return { success: true, message: 'Configuration updated successfully' }; + } else { + return { success: false, error: result.error || 'Failed to update configuration', status: 500 }; + } + } catch (err) { + return { success: false, error: err.message, status: 500 }; + } + }); + return true; + } + + // API: CodexLens Clean (Clean indexes) + if (pathname === '/api/codexlens/clean' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { all = false, path } = body; + + try { + const args = ['clean']; + if (all) { + args.push('--all'); + } + if (path) { + args.push('--path', path); + } + args.push('--json'); + + const result = await executeCodexLens(args); + if (result.success) { + return { success: true, message: 'Indexes cleaned successfully' }; + } else { + return { success: false, error: result.error || 'Failed to clean indexes', status: 500 }; + } + } catch (err) { + return { success: false, error: err.message, status: 500 }; + } + }); + return true; + } + // API: CodexLens Init (Initialize workspace index) if (pathname === '/api/codexlens/init' && req.method === 'POST') { handlePostRequest(req, res, async (body) => { diff --git a/ccw/src/core/routes/mcp-routes.ts b/ccw/src/core/routes/mcp-routes.ts index 82339801..f2251f1d 100644 --- a/ccw/src/core/routes/mcp-routes.ts +++ b/ccw/src/core/routes/mcp-routes.ts @@ -4,13 +4,17 @@ * Handles all MCP-related API endpoints */ import type { IncomingMessage, ServerResponse } from 'http'; -import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs'; import { join, dirname } from 'path'; import { homedir } from 'os'; +import * as McpTemplatesDb from './mcp-templates-db.js'; // Claude config file path const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json'); +// Workspace root path for scanning .mcp.json files +let WORKSPACE_ROOT = process.cwd(); + export interface RouteContext { pathname: string; url: URL; @@ -64,6 +68,83 @@ function getMcpServersFromFile(filePath) { return config.mcpServers || {}; } +/** + * Add or update MCP server in project's .mcp.json file + * @param {string} projectPath - Project directory path + * @param {string} serverName - MCP server name + * @param {Object} serverConfig - MCP server configuration + * @returns {Object} Result with success/error + */ +function addMcpServerToMcpJson(projectPath, serverName, serverConfig) { + try { + const normalizedPath = normalizeProjectPathForConfig(projectPath); + const mcpJsonPath = join(normalizedPath, '.mcp.json'); + + // Read existing .mcp.json or create new structure + let mcpJson = safeReadJson(mcpJsonPath) || { mcpServers: {} }; + + // Ensure mcpServers exists + if (!mcpJson.mcpServers) { + mcpJson.mcpServers = {}; + } + + // Add or update the server + mcpJson.mcpServers[serverName] = serverConfig; + + // Write back to .mcp.json + writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2), 'utf8'); + + return { + success: true, + serverName, + serverConfig, + scope: 'project-mcp-json', + path: mcpJsonPath + }; + } catch (error: unknown) { + console.error('Error adding MCP server to .mcp.json:', error); + return { error: (error as Error).message }; + } +} + +/** + * Remove MCP server from project's .mcp.json file + * @param {string} projectPath - Project directory path + * @param {string} serverName - MCP server name + * @returns {Object} Result with success/error + */ +function removeMcpServerFromMcpJson(projectPath, serverName) { + try { + const normalizedPath = normalizeProjectPathForConfig(projectPath); + const mcpJsonPath = join(normalizedPath, '.mcp.json'); + + if (!existsSync(mcpJsonPath)) { + return { error: '.mcp.json not found' }; + } + + const mcpJson = safeReadJson(mcpJsonPath); + if (!mcpJson || !mcpJson.mcpServers || !mcpJson.mcpServers[serverName]) { + return { error: `Server not found: ${serverName}` }; + } + + // Remove the server + delete mcpJson.mcpServers[serverName]; + + // Write back to .mcp.json + writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2), 'utf8'); + + return { + success: true, + serverName, + removed: true, + scope: 'project-mcp-json' + }; + } catch (error: unknown) { + console.error('Error removing MCP server from .mcp.json:', error); + return { error: (error as Error).message }; + } +} + /** * Get MCP configuration from multiple sources (per official Claude Code docs): * @@ -114,6 +195,7 @@ function getMcpConfig() { } // 3. For each known project, check for .mcp.json (project-level config) + // .mcp.json is now the PRIMARY source for project-level MCP servers const projectPaths = Object.keys(result.projects); for (const projectPath of projectPaths) { const mcpJsonPath = join(projectPath, '.mcp.json'); @@ -121,17 +203,22 @@ function getMcpConfig() { const mcpJsonConfig = safeReadJson(mcpJsonPath); if (mcpJsonConfig?.mcpServers) { // Merge .mcp.json servers into project config - // Project's .mcp.json has lower priority than ~/.claude.json projects[path].mcpServers + // .mcp.json has HIGHER priority than ~/.claude.json projects[path].mcpServers const existingServers = result.projects[projectPath]?.mcpServers || {}; result.projects[projectPath] = { ...result.projects[projectPath], mcpServers: { - ...mcpJsonConfig.mcpServers, // .mcp.json (lower priority) - ...existingServers // ~/.claude.json projects[path] (higher priority) + ...existingServers, // ~/.claude.json projects[path] (lower priority, legacy) + ...mcpJsonConfig.mcpServers // .mcp.json (higher priority, new default) }, - mcpJsonPath: mcpJsonPath // Track source for debugging + mcpJsonPath: mcpJsonPath, // Track source for debugging + hasMcpJson: true }; - result.configSources.push({ type: 'project-mcp-json', path: mcpJsonPath, count: Object.keys(mcpJsonConfig.mcpServers).length }); + result.configSources.push({ + type: 'project-mcp-json', + path: mcpJsonPath, + count: Object.keys(mcpJsonConfig.mcpServers).length + }); } } } @@ -223,13 +310,21 @@ function toggleMcpServerEnabled(projectPath, serverName, enable) { /** * Add MCP server to project + * Now defaults to using .mcp.json instead of .claude.json * @param {string} projectPath * @param {string} serverName * @param {Object} serverConfig + * @param {boolean} useLegacyConfig - If true, use .claude.json instead of .mcp.json * @returns {Object} */ -function addMcpServerToProject(projectPath, serverName, serverConfig) { +function addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyConfig = false) { try { + // Default: Use .mcp.json for project-level MCP servers + if (!useLegacyConfig) { + return addMcpServerToMcpJson(projectPath, serverName, serverConfig); + } + + // Legacy: Use .claude.json (kept for backward compatibility) if (!existsSync(CLAUDE_CONFIG_PATH)) { return { error: '.claude.json not found' }; } @@ -274,7 +369,8 @@ function addMcpServerToProject(projectPath, serverName, serverConfig) { return { success: true, serverName, - serverConfig + serverConfig, + scope: 'project-legacy' }; } catch (error: unknown) { console.error('Error adding MCP server:', error); @@ -284,11 +380,80 @@ function addMcpServerToProject(projectPath, serverName, serverConfig) { /** * Remove MCP server from project + * Checks both .mcp.json and .claude.json * @param {string} projectPath * @param {string} serverName * @returns {Object} */ function removeMcpServerFromProject(projectPath, serverName) { + try { + const normalizedPath = normalizeProjectPathForConfig(projectPath); + const mcpJsonPath = join(normalizedPath, '.mcp.json'); + + let removedFromMcpJson = false; + let removedFromClaudeJson = false; + + // Try to remove from .mcp.json first (new default) + if (existsSync(mcpJsonPath)) { + const mcpJson = safeReadJson(mcpJsonPath); + if (mcpJson?.mcpServers?.[serverName]) { + const result = removeMcpServerFromMcpJson(projectPath, serverName); + if (result.success) { + removedFromMcpJson = true; + } + } + } + + // Also try to remove from .claude.json (legacy - may coexist) + if (existsSync(CLAUDE_CONFIG_PATH)) { + const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8'); + const config = JSON.parse(content); + + if (config.projects && config.projects[normalizedPath]) { + const projectConfig = config.projects[normalizedPath]; + + if (projectConfig.mcpServers && projectConfig.mcpServers[serverName]) { + // Remove the server + delete projectConfig.mcpServers[serverName]; + + // Also remove from disabled list if present + if (projectConfig.disabledMcpServers) { + projectConfig.disabledMcpServers = projectConfig.disabledMcpServers.filter(s => s !== serverName); + } + + // Write back to file + writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); + removedFromClaudeJson = true; + } + } + } + + // Return success if removed from either location + if (removedFromMcpJson || removedFromClaudeJson) { + return { + success: true, + serverName, + removed: true, + scope: removedFromMcpJson ? 'project-mcp-json' : 'project-legacy', + removedFrom: removedFromMcpJson && removedFromClaudeJson ? 'both' : + removedFromMcpJson ? '.mcp.json' : '.claude.json' + }; + } + + return { error: `Server not found: ${serverName}` }; + } catch (error: unknown) { + console.error('Error removing MCP server:', error); + return { error: (error as Error).message }; + } +} + +/** + * Add MCP server to global/user scope (top-level mcpServers in ~/.claude.json) + * @param {string} serverName + * @param {Object} serverConfig + * @returns {Object} + */ +function addGlobalMcpServer(serverName, serverConfig) { try { if (!existsSync(CLAUDE_CONFIG_PATH)) { return { error: '.claude.json not found' }; @@ -297,25 +462,13 @@ function removeMcpServerFromProject(projectPath, serverName) { const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8'); const config = JSON.parse(content); - const normalizedPath = normalizeProjectPathForConfig(projectPath); - - if (!config.projects || !config.projects[normalizedPath]) { - return { error: `Project not found: ${normalizedPath}` }; + // Ensure top-level mcpServers exists + if (!config.mcpServers) { + config.mcpServers = {}; } - const projectConfig = config.projects[normalizedPath]; - - if (!projectConfig.mcpServers || !projectConfig.mcpServers[serverName]) { - return { error: `Server not found: ${serverName}` }; - } - - // Remove the server - delete projectConfig.mcpServers[serverName]; - - // Also remove from disabled list if present - if (projectConfig.disabledMcpServers) { - projectConfig.disabledMcpServers = projectConfig.disabledMcpServers.filter(s => s !== serverName); - } + // Add the server to top-level mcpServers + config.mcpServers[serverName] = serverConfig; // Write back to file writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); @@ -323,10 +476,47 @@ function removeMcpServerFromProject(projectPath, serverName) { return { success: true, serverName, - removed: true + serverConfig, + scope: 'global' }; } catch (error: unknown) { - console.error('Error removing MCP server:', error); + console.error('Error adding global MCP server:', error); + return { error: (error as Error).message }; + } +} + +/** + * Remove MCP server from global/user scope (top-level mcpServers) + * @param {string} serverName + * @returns {Object} + */ +function removeGlobalMcpServer(serverName) { + try { + if (!existsSync(CLAUDE_CONFIG_PATH)) { + return { error: '.claude.json not found' }; + } + + const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8'); + const config = JSON.parse(content); + + if (!config.mcpServers || !config.mcpServers[serverName]) { + return { error: `Global server not found: ${serverName}` }; + } + + // Remove the server from top-level mcpServers + delete config.mcpServers[serverName]; + + // Write back to file + writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); + + return { + success: true, + serverName, + removed: true, + scope: 'global' + }; + } catch (error: unknown) { + console.error('Error removing global MCP server:', error); return { error: (error as Error).message }; } } @@ -448,5 +638,134 @@ export async function handleMcpRoutes(ctx: RouteContext): Promise { return true; } + // API: Add MCP server to global scope (top-level mcpServers in ~/.claude.json) + if (pathname === '/api/mcp-add-global-server' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { serverName, serverConfig } = body; + if (!serverName || !serverConfig) { + return { error: 'serverName and serverConfig are required', status: 400 }; + } + return addGlobalMcpServer(serverName, serverConfig); + }); + return true; + } + + // API: Remove MCP server from global scope + if (pathname === '/api/mcp-remove-global-server' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { serverName } = body; + if (!serverName) { + return { error: 'serverName is required', status: 400 }; + } + return removeGlobalMcpServer(serverName); + }); + return true; + } + + // ======================================== + // MCP Templates API + // ======================================== + + // API: Get all MCP templates + if (pathname === '/api/mcp-templates' && req.method === 'GET') { + const templates = McpTemplatesDb.getAllTemplates(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, templates })); + return true; + } + + // API: Save MCP template + if (pathname === '/api/mcp-templates' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { name, description, serverConfig, tags, category } = body; + if (!name || !serverConfig) { + return { error: 'name and serverConfig are required', status: 400 }; + } + return McpTemplatesDb.saveTemplate({ + name, + description, + serverConfig, + tags, + category + }); + }); + return true; + } + + // API: Get template by name + if (pathname.startsWith('/api/mcp-templates/') && req.method === 'GET') { + const templateName = decodeURIComponent(pathname.split('/api/mcp-templates/')[1]); + const template = McpTemplatesDb.getTemplateByName(templateName); + if (template) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, template })); + } else { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Template not found' })); + } + return true; + } + + // API: Delete MCP template + if (pathname.startsWith('/api/mcp-templates/') && req.method === 'DELETE') { + const templateName = decodeURIComponent(pathname.split('/api/mcp-templates/')[1]); + const result = McpTemplatesDb.deleteTemplate(templateName); + res.writeHead(result.success ? 200 : 404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + return true; + } + + // API: Search MCP templates + if (pathname === '/api/mcp-templates/search' && req.method === 'GET') { + const keyword = url.searchParams.get('q') || ''; + const templates = McpTemplatesDb.searchTemplates(keyword); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, templates })); + return true; + } + + // API: Get all categories + if (pathname === '/api/mcp-templates/categories' && req.method === 'GET') { + const categories = McpTemplatesDb.getAllCategories(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, categories })); + return true; + } + + // API: Get templates by category + if (pathname.startsWith('/api/mcp-templates/category/') && req.method === 'GET') { + const category = decodeURIComponent(pathname.split('/api/mcp-templates/category/')[1]); + const templates = McpTemplatesDb.getTemplatesByCategory(category); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, templates })); + return true; + } + + // API: Install template to project or global + if (pathname === '/api/mcp-templates/install' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { templateName, projectPath, scope } = body; + if (!templateName) { + return { error: 'templateName is required', status: 400 }; + } + + const template = McpTemplatesDb.getTemplateByName(templateName); + if (!template) { + return { error: 'Template not found', status: 404 }; + } + + // Install to global or project + if (scope === 'global') { + return addGlobalMcpServer(templateName, template.serverConfig); + } else { + if (!projectPath) { + return { error: 'projectPath is required for project scope', status: 400 }; + } + return addMcpServerToProject(projectPath, templateName, template.serverConfig); + } + }); + return true; + } + return false; } diff --git a/ccw/src/core/routes/mcp-routes.ts.backup b/ccw/src/core/routes/mcp-routes.ts.backup new file mode 100644 index 00000000..2a53d079 --- /dev/null +++ b/ccw/src/core/routes/mcp-routes.ts.backup @@ -0,0 +1,550 @@ +// @ts-nocheck +/** + * MCP Routes Module + * Handles all MCP-related API endpoints + */ +import type { IncomingMessage, ServerResponse } from 'http'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { homedir } from 'os'; + +// Claude config file path +const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json'); + +export interface RouteContext { + pathname: string; + url: URL; + req: IncomingMessage; + res: ServerResponse; + initialPath: string; + handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise) => void; + broadcastToClients: (data: unknown) => void; +} + +// ======================================== +// Helper Functions +// ======================================== + +/** + * Get enterprise managed MCP path (platform-specific) + */ +function getEnterpriseMcpPath(): string { + const platform = process.platform; + if (platform === 'darwin') { + return '/Library/Application Support/ClaudeCode/managed-mcp.json'; + } else if (platform === 'win32') { + return 'C:\\Program Files\\ClaudeCode\\managed-mcp.json'; + } else { + // Linux and WSL + return '/etc/claude-code/managed-mcp.json'; + } +} + +/** + * Safely read and parse JSON file + */ +function safeReadJson(filePath) { + try { + if (!existsSync(filePath)) return null; + const content = readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } catch { + return null; + } +} + +/** + * Get MCP servers from a JSON file (expects mcpServers key at top level) + * @param {string} filePath + * @returns {Object} mcpServers object or empty object + */ +function getMcpServersFromFile(filePath) { + const config = safeReadJson(filePath); + if (!config) return {}; + return config.mcpServers || {}; +} + +/** + * Get MCP configuration from multiple sources (per official Claude Code docs): + * + * Priority (highest to lowest): + * 1. Enterprise managed-mcp.json (cannot be overridden) + * 2. Local scope (project-specific private in ~/.claude.json) + * 3. Project scope (.mcp.json in project root) + * 4. User scope (mcpServers in ~/.claude.json) + * + * Note: ~/.claude/settings.json is for MCP PERMISSIONS, NOT definitions! + * + * @returns {Object} + */ +function getMcpConfig() { + try { + const result = { + projects: {}, + userServers: {}, // User-level servers from ~/.claude.json mcpServers + enterpriseServers: {}, // Enterprise managed servers (highest priority) + configSources: [] // Track where configs came from for debugging + }; + + // 1. Read Enterprise managed MCP servers (highest priority) + const enterprisePath = getEnterpriseMcpPath(); + if (existsSync(enterprisePath)) { + const enterpriseConfig = safeReadJson(enterprisePath); + if (enterpriseConfig?.mcpServers) { + result.enterpriseServers = enterpriseConfig.mcpServers; + result.configSources.push({ type: 'enterprise', path: enterprisePath, count: Object.keys(enterpriseConfig.mcpServers).length }); + } + } + + // 2. Read from ~/.claude.json + if (existsSync(CLAUDE_CONFIG_PATH)) { + const claudeConfig = safeReadJson(CLAUDE_CONFIG_PATH); + if (claudeConfig) { + // 2a. User-level mcpServers (top-level mcpServers key) + if (claudeConfig.mcpServers) { + result.userServers = claudeConfig.mcpServers; + result.configSources.push({ type: 'user', path: CLAUDE_CONFIG_PATH, count: Object.keys(claudeConfig.mcpServers).length }); + } + + // 2b. Project-specific configurations (projects[path].mcpServers) + if (claudeConfig.projects) { + result.projects = claudeConfig.projects; + } + } + } + + // 3. For each known project, check for .mcp.json (project-level config) + const projectPaths = Object.keys(result.projects); + for (const projectPath of projectPaths) { + const mcpJsonPath = join(projectPath, '.mcp.json'); + if (existsSync(mcpJsonPath)) { + const mcpJsonConfig = safeReadJson(mcpJsonPath); + if (mcpJsonConfig?.mcpServers) { + // Merge .mcp.json servers into project config + // Project's .mcp.json has lower priority than ~/.claude.json projects[path].mcpServers + const existingServers = result.projects[projectPath]?.mcpServers || {}; + result.projects[projectPath] = { + ...result.projects[projectPath], + mcpServers: { + ...mcpJsonConfig.mcpServers, // .mcp.json (lower priority) + ...existingServers // ~/.claude.json projects[path] (higher priority) + }, + mcpJsonPath: mcpJsonPath // Track source for debugging + }; + result.configSources.push({ type: 'project-mcp-json', path: mcpJsonPath, count: Object.keys(mcpJsonConfig.mcpServers).length }); + } + } + } + + // Build globalServers by merging user and enterprise servers + // Enterprise servers override user servers + result.globalServers = { + ...result.userServers, + ...result.enterpriseServers + }; + + return result; + } catch (error: unknown) { + console.error('Error reading MCP config:', error); + return { projects: {}, globalServers: {}, userServers: {}, enterpriseServers: {}, configSources: [], error: (error as Error).message }; + } +} + +/** + * Normalize project path for .claude.json (Windows backslash format) + * @param {string} path + * @returns {string} + */ +function normalizeProjectPathForConfig(path) { + // Convert forward slashes to backslashes for Windows .claude.json format + let normalized = path.replace(/\//g, '\\'); + + // Handle /d/path format -> D:\path + if (normalized.match(/^\\[a-zA-Z]\\/)) { + normalized = normalized.charAt(1).toUpperCase() + ':' + normalized.slice(2); + } + + return normalized; +} + +/** + * Toggle MCP server enabled/disabled + * @param {string} projectPath + * @param {string} serverName + * @param {boolean} enable + * @returns {Object} + */ +function toggleMcpServerEnabled(projectPath, serverName, enable) { + try { + if (!existsSync(CLAUDE_CONFIG_PATH)) { + return { error: '.claude.json not found' }; + } + + const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8'); + const config = JSON.parse(content); + + const normalizedPath = normalizeProjectPathForConfig(projectPath); + + if (!config.projects || !config.projects[normalizedPath]) { + return { error: `Project not found: ${normalizedPath}` }; + } + + const projectConfig = config.projects[normalizedPath]; + + // Ensure disabledMcpServers array exists + if (!projectConfig.disabledMcpServers) { + projectConfig.disabledMcpServers = []; + } + + if (enable) { + // Remove from disabled list + projectConfig.disabledMcpServers = projectConfig.disabledMcpServers.filter(s => s !== serverName); + } else { + // Add to disabled list if not already there + if (!projectConfig.disabledMcpServers.includes(serverName)) { + projectConfig.disabledMcpServers.push(serverName); + } + } + + // Write back to file + writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); + + return { + success: true, + serverName, + enabled: enable, + disabledMcpServers: projectConfig.disabledMcpServers + }; + } catch (error: unknown) { + console.error('Error toggling MCP server:', error); + return { error: (error as Error).message }; + } +} + +/** + * Add MCP server to project + * @param {string} projectPath + * @param {string} serverName + * @param {Object} serverConfig + * @returns {Object} + */ +function addMcpServerToProject(projectPath, serverName, serverConfig) { + try { + if (!existsSync(CLAUDE_CONFIG_PATH)) { + return { error: '.claude.json not found' }; + } + + const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8'); + const config = JSON.parse(content); + + const normalizedPath = normalizeProjectPathForConfig(projectPath); + + // Create project entry if it doesn't exist + if (!config.projects) { + config.projects = {}; + } + + if (!config.projects[normalizedPath]) { + config.projects[normalizedPath] = { + allowedTools: [], + mcpContextUris: [], + mcpServers: {}, + enabledMcpjsonServers: [], + disabledMcpjsonServers: [], + hasTrustDialogAccepted: false, + projectOnboardingSeenCount: 0, + hasClaudeMdExternalIncludesApproved: false, + hasClaudeMdExternalIncludesWarningShown: false + }; + } + + const projectConfig = config.projects[normalizedPath]; + + // Ensure mcpServers exists + if (!projectConfig.mcpServers) { + projectConfig.mcpServers = {}; + } + + // Add the server + projectConfig.mcpServers[serverName] = serverConfig; + + // Write back to file + writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); + + return { + success: true, + serverName, + serverConfig + }; + } catch (error: unknown) { + console.error('Error adding MCP server:', error); + return { error: (error as Error).message }; + } +} + +/** + * Remove MCP server from project + * @param {string} projectPath + * @param {string} serverName + * @returns {Object} + */ +function removeMcpServerFromProject(projectPath, serverName) { + try { + if (!existsSync(CLAUDE_CONFIG_PATH)) { + return { error: '.claude.json not found' }; + } + + const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8'); + const config = JSON.parse(content); + + const normalizedPath = normalizeProjectPathForConfig(projectPath); + + if (!config.projects || !config.projects[normalizedPath]) { + return { error: `Project not found: ${normalizedPath}` }; + } + + const projectConfig = config.projects[normalizedPath]; + + if (!projectConfig.mcpServers || !projectConfig.mcpServers[serverName]) { + return { error: `Server not found: ${serverName}` }; + } + + // Remove the server + delete projectConfig.mcpServers[serverName]; + + // Also remove from disabled list if present + if (projectConfig.disabledMcpServers) { + projectConfig.disabledMcpServers = projectConfig.disabledMcpServers.filter(s => s !== serverName); + } + + // Write back to file + writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); + + return { + success: true, + serverName, + removed: true + }; + } catch (error: unknown) { + console.error('Error removing MCP server:', error); + return { error: (error as Error).message }; + } +} + +/** + * Add MCP server to global/user scope (top-level mcpServers in ~/.claude.json) + * @param {string} serverName + * @param {Object} serverConfig + * @returns {Object} + */ +function addGlobalMcpServer(serverName, serverConfig) { + try { + if (!existsSync(CLAUDE_CONFIG_PATH)) { + return { error: '.claude.json not found' }; + } + + const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8'); + const config = JSON.parse(content); + + // Ensure top-level mcpServers exists + if (!config.mcpServers) { + config.mcpServers = {}; + } + + // Add the server to top-level mcpServers + config.mcpServers[serverName] = serverConfig; + + // Write back to file + writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); + + return { + success: true, + serverName, + serverConfig, + scope: 'global' + }; + } catch (error: unknown) { + console.error('Error adding global MCP server:', error); + return { error: (error as Error).message }; + } +} + +/** + * Remove MCP server from global/user scope (top-level mcpServers) + * @param {string} serverName + * @returns {Object} + */ +function removeGlobalMcpServer(serverName) { + try { + if (!existsSync(CLAUDE_CONFIG_PATH)) { + return { error: '.claude.json not found' }; + } + + const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8'); + const config = JSON.parse(content); + + if (!config.mcpServers || !config.mcpServers[serverName]) { + return { error: `Global server not found: ${serverName}` }; + } + + // Remove the server from top-level mcpServers + delete config.mcpServers[serverName]; + + // Write back to file + writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); + + return { + success: true, + serverName, + removed: true, + scope: 'global' + }; + } catch (error: unknown) { + console.error('Error removing global MCP server:', error); + return { error: (error as Error).message }; + } +} + +/** + * Read settings file safely + * @param {string} filePath + * @returns {Object} + */ +function readSettingsFile(filePath) { + try { + if (!existsSync(filePath)) { + return {}; + } + const content = readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } catch (error: unknown) { + console.error(`Error reading settings file ${filePath}:`, error); + return {}; + } +} + +/** + * Write settings file safely + * @param {string} filePath + * @param {Object} settings + */ +function writeSettingsFile(filePath, settings) { + const dirPath = dirname(filePath); + // Ensure directory exists + if (!existsSync(dirPath)) { + mkdirSync(dirPath, { recursive: true }); + } + writeFileSync(filePath, JSON.stringify(settings, null, 2), 'utf8'); +} + +/** + * Get project settings path + * @param {string} projectPath + * @returns {string} + */ +function getProjectSettingsPath(projectPath) { + const normalizedPath = projectPath.replace(/\//g, '\\').replace(/^\\([a-zA-Z])\\/, '$1:\\'); + return join(normalizedPath, '.claude', 'settings.json'); +} + +// ======================================== +// Route Handlers +// ======================================== + +/** + * Handle MCP routes + * @returns true if route was handled, false otherwise + */ +export async function handleMcpRoutes(ctx: RouteContext): Promise { + const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx; + + // API: Get MCP configuration + if (pathname === '/api/mcp-config') { + const mcpData = getMcpConfig(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(mcpData)); + return true; + } + + // API: Toggle MCP server enabled/disabled + if (pathname === '/api/mcp-toggle' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { projectPath, serverName, enable } = body; + if (!projectPath || !serverName) { + return { error: 'projectPath and serverName are required', status: 400 }; + } + return toggleMcpServerEnabled(projectPath, serverName, enable); + }); + return true; + } + + // API: Copy MCP server to project + if (pathname === '/api/mcp-copy-server' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { projectPath, serverName, serverConfig } = body; + if (!projectPath || !serverName || !serverConfig) { + return { error: 'projectPath, serverName, and serverConfig are required', status: 400 }; + } + return addMcpServerToProject(projectPath, serverName, serverConfig); + }); + return true; + } + + // API: Install CCW MCP server to project + if (pathname === '/api/mcp-install-ccw' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { projectPath } = body; + if (!projectPath) { + return { error: 'projectPath is required', status: 400 }; + } + + // Generate CCW MCP server config + const ccwMcpConfig = { + command: "ccw-mcp", + args: [] + }; + + // Use existing addMcpServerToProject to install CCW MCP + return addMcpServerToProject(projectPath, 'ccw-mcp', ccwMcpConfig); + }); + return true; + } + + // API: Remove MCP server from project + if (pathname === '/api/mcp-remove-server' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { projectPath, serverName } = body; + if (!projectPath || !serverName) { + return { error: 'projectPath and serverName are required', status: 400 }; + } + return removeMcpServerFromProject(projectPath, serverName); + }); + return true; + } + + // API: Add MCP server to global scope (top-level mcpServers in ~/.claude.json) + if (pathname === '/api/mcp-add-global-server' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { serverName, serverConfig } = body; + if (!serverName || !serverConfig) { + return { error: 'serverName and serverConfig are required', status: 400 }; + } + return addGlobalMcpServer(serverName, serverConfig); + }); + return true; + } + + // API: Remove MCP server from global scope + if (pathname === '/api/mcp-remove-global-server' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { serverName } = body; + if (!serverName) { + return { error: 'serverName is required', status: 400 }; + } + return removeGlobalMcpServer(serverName); + }); + return true; + } + + return false; +} diff --git a/ccw/src/core/routes/rules-routes.ts b/ccw/src/core/routes/rules-routes.ts index f516898f..09d68661 100644 --- a/ccw/src/core/routes/rules-routes.ts +++ b/ccw/src/core/routes/rules-routes.ts @@ -4,9 +4,10 @@ * Handles all Rules-related API endpoints */ import type { IncomingMessage, ServerResponse } from 'http'; -import { readFileSync, existsSync, readdirSync, unlinkSync } from 'fs'; +import { readFileSync, existsSync, readdirSync, unlinkSync, promises as fsPromises } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; +import { executeCliTool } from '../../tools/cli-executor.js'; export interface RouteContext { pathname: string; @@ -220,6 +221,191 @@ function deleteRule(ruleName, location, projectPath) { } } +/** + * Generate rule content via CLI tool + * @param {Object} params + * @param {string} params.generationType - 'description' | 'template' | 'extract' + * @param {string} params.description - Rule description (for 'description' mode) + * @param {string} params.templateType - Template type (for 'template' mode) + * @param {string} params.extractScope - Scope pattern (for 'extract' mode) + * @param {string} params.extractFocus - Focus areas (for 'extract' mode) + * @param {string} params.fileName - Target file name + * @param {string} params.location - 'project' or 'user' + * @param {string} params.subdirectory - Optional subdirectory + * @param {string} params.projectPath - Project root path + * @returns {Object} + */ +async function generateRuleViaCLI(params) { + try { + const { + generationType, + description, + templateType, + extractScope, + extractFocus, + fileName, + location, + subdirectory, + projectPath + } = params; + + let prompt = ''; + let mode = 'analysis'; + let workingDir = projectPath; + + // Build prompt based on generation type + if (generationType === 'description') { + mode = 'write'; + prompt = `PURPOSE: Generate Claude Code memory rule from description to guide Claude's behavior +TASK: • Analyze rule requirements • Generate markdown content with clear instructions +MODE: write +EXPECTED: Complete rule content in markdown format +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) | Follow Claude Code rule format | Use frontmatter for conditional rules if paths specified | write=CREATE + +RULE DESCRIPTION: +${description} + +FILE NAME: ${fileName}`; + } else if (generationType === 'template') { + mode = 'write'; + prompt = `PURPOSE: Generate Claude Code rule from template type +TASK: • Create rule based on ${templateType} template • Generate structured markdown content +MODE: write +EXPECTED: Complete rule content in markdown format following template structure +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) | Follow Claude Code rule format | Use ${templateType} template patterns | write=CREATE + +TEMPLATE TYPE: ${templateType} +FILE NAME: ${fileName}`; + } else if (generationType === 'extract') { + mode = 'analysis'; + prompt = `PURPOSE: Extract coding rules from existing codebase to document patterns and conventions +TASK: • Analyze code patterns in specified scope • Extract common conventions • Identify best practices +MODE: analysis +CONTEXT: @${extractScope || '**/*'} +EXPECTED: Rule content based on codebase analysis with examples +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/analysis/02-analyze-code-patterns.txt) | Focus on actual patterns found | Include code examples | analysis=READ-ONLY + +ANALYSIS SCOPE: ${extractScope || '**/*'} +FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structure'}`; + } else { + return { error: `Unknown generation type: ${generationType}` }; + } + + // Execute CLI tool (Gemini) with at least 10 minutes timeout + const result = await executeCliTool({ + tool: 'gemini', + prompt, + mode, + cd: workingDir, + timeout: 600000 // 10 minutes + }); + + if (!result.success) { + return { + error: `CLI execution failed: ${result.stderr || 'Unknown error'}`, + stderr: result.stderr + }; + } + + // Extract generated content from stdout + const generatedContent = result.stdout.trim(); + + if (!generatedContent) { + return { + error: 'CLI execution returned empty content', + stdout: result.stdout, + stderr: result.stderr + }; + } + + // Create the rule using the generated content + const createResult = await createRule({ + fileName, + content: generatedContent, + paths: [], + location, + subdirectory, + projectPath + }); + + return { + success: createResult.success || false, + ...createResult, + generatedContent, + executionId: result.conversation?.id + }; + } catch (error) { + return { error: (error as Error).message }; + } +} + +/** + * Create a new rule + * @param {Object} params + * @param {string} params.fileName - Rule file name (must end with .md) + * @param {string} params.content - Rule content (markdown) + * @param {string[]} params.paths - Optional paths for conditional rule + * @param {string} params.location - 'project' or 'user' + * @param {string} params.subdirectory - Optional subdirectory path + * @param {string} params.projectPath - Project root path + * @returns {Object} + */ +async function createRule(params) { + try { + const { fileName, content, paths, location, subdirectory, projectPath } = params; + + // Validate file name + if (!fileName || !fileName.endsWith('.md')) { + return { error: 'File name must end with .md' }; + } + + // Build base directory + const baseDir = location === 'project' + ? join(projectPath, '.claude', 'rules') + : join(homedir(), '.claude', 'rules'); + + // Build target directory (with optional subdirectory) + const targetDir = subdirectory + ? join(baseDir, subdirectory) + : baseDir; + + // Ensure target directory exists + await fsPromises.mkdir(targetDir, { recursive: true }); + + // Build complete file path + const filePath = join(targetDir, fileName); + + // Check if file already exists + if (existsSync(filePath)) { + return { error: `Rule '${fileName}' already exists in ${location} location` }; + } + + // Build complete content with frontmatter if paths provided + let completeContent = content; + if (paths && paths.length > 0) { + const frontmatter = `--- +paths: [${paths.join(', ')}] +--- + +`; + completeContent = frontmatter + content; + } + + // Write rule file + await fsPromises.writeFile(filePath, completeContent, 'utf8'); + + return { + success: true, + fileName, + location, + path: filePath, + subdirectory: subdirectory || null + }; + } catch (error) { + return { error: (error as Error).message }; + } +} + /** * Handle Rules routes * @returns true if route was handled, false otherwise @@ -262,5 +448,79 @@ export async function handleRulesRoutes(ctx: RouteContext): Promise { return true; } + // API: Create rule + if (pathname === '/api/rules/create' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { + mode, + fileName, + content, + paths, + location, + subdirectory, + projectPath: projectPathParam, + // CLI generation parameters + generationType, + description, + templateType, + extractScope, + extractFocus + } = body; + + if (!fileName) { + return { error: 'File name is required' }; + } + + if (!location) { + return { error: 'Location is required (project or user)' }; + } + + const projectPath = projectPathParam || initialPath; + + // CLI generation mode + if (mode === 'cli-generate') { + if (!generationType) { + return { error: 'generationType is required for CLI generation mode' }; + } + + // Validate based on generation type + if (generationType === 'description' && !description) { + return { error: 'description is required for description-based generation' }; + } + + if (generationType === 'template' && !templateType) { + return { error: 'templateType is required for template-based generation' }; + } + + return await generateRuleViaCLI({ + generationType, + description, + templateType, + extractScope, + extractFocus, + fileName, + location, + subdirectory: subdirectory || '', + projectPath + }); + } + + // Manual creation mode + if (!content) { + return { error: 'Content is required for manual creation' }; + } + + return await createRule({ + fileName, + content, + paths: paths || [], + location, + subdirectory: subdirectory || '', + projectPath + }); + }); + return true; + } + return false; } diff --git a/ccw/src/core/routes/skills-routes.ts b/ccw/src/core/routes/skills-routes.ts index 4d130f87..784188e2 100644 --- a/ccw/src/core/routes/skills-routes.ts +++ b/ccw/src/core/routes/skills-routes.ts @@ -7,6 +7,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { readFileSync, existsSync, readdirSync, statSync, unlinkSync, promises as fsPromises } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; +import { executeCliTool } from '../../tools/cli-executor.js'; export interface RouteContext { pathname: string; @@ -252,6 +253,250 @@ function deleteSkill(skillName, location, projectPath) { } } +/** + * Validate skill folder structure + * @param {string} folderPath - Path to skill folder + * @returns {Object} Validation result with skill info + */ +function validateSkillFolder(folderPath) { + const errors = []; + + // Check if folder exists + if (!existsSync(folderPath)) { + return { valid: false, errors: ['Folder does not exist'], skillInfo: null }; + } + + // Check if it's a directory + try { + const stat = statSync(folderPath); + if (!stat.isDirectory()) { + return { valid: false, errors: ['Path is not a directory'], skillInfo: null }; + } + } catch (e) { + return { valid: false, errors: ['Cannot access folder'], skillInfo: null }; + } + + // Check SKILL.md exists + const skillMdPath = join(folderPath, 'SKILL.md'); + if (!existsSync(skillMdPath)) { + errors.push('SKILL.md file not found'); + return { valid: false, errors, skillInfo: null }; + } + + // Parse and validate frontmatter + try { + const content = readFileSync(skillMdPath, 'utf8'); + const parsed = parseSkillFrontmatter(content); + + if (!parsed.name) { + errors.push('name field is required in frontmatter'); + } + if (!parsed.description) { + errors.push('description field is required in frontmatter'); + } + + // Get supporting files + const supportingFiles = getSupportingFiles(folderPath); + + // If validation passed + if (errors.length === 0) { + return { + valid: true, + errors: [], + skillInfo: { + name: parsed.name, + description: parsed.description, + version: parsed.version, + allowedTools: parsed.allowedTools, + supportingFiles + } + }; + } else { + return { valid: false, errors, skillInfo: null }; + } + } catch (error) { + return { valid: false, errors: ['Failed to parse SKILL.md: ' + (error as Error).message], skillInfo: null }; + } +} + +/** + * Recursively copy directory + * @param {string} source - Source directory path + * @param {string} target - Target directory path + */ +async function copyDirectoryRecursive(source, target) { + await fsPromises.mkdir(target, { recursive: true }); + + const entries = await fsPromises.readdir(source, { withFileTypes: true }); + + for (const entry of entries) { + const sourcePath = join(source, entry.name); + const targetPath = join(target, entry.name); + + if (entry.isDirectory()) { + await copyDirectoryRecursive(sourcePath, targetPath); + } else { + await fsPromises.copyFile(sourcePath, targetPath); + } + } +} + +/** + * Import skill from folder + * @param {string} sourcePath - Source skill folder path + * @param {string} location - 'project' or 'user' + * @param {string} projectPath - Project root path + * @param {string} customName - Optional custom name for skill + * @returns {Object} + */ +async function importSkill(sourcePath, location, projectPath, customName) { + try { + // Validate source folder + const validation = validateSkillFolder(sourcePath); + if (!validation.valid) { + return { error: validation.errors.join(', ') }; + } + + const baseDir = location === 'project' + ? join(projectPath, '.claude', 'skills') + : join(homedir(), '.claude', 'skills'); + + // Ensure base directory exists + if (!existsSync(baseDir)) { + await fsPromises.mkdir(baseDir, { recursive: true }); + } + + // Determine target folder name + const skillName = customName || validation.skillInfo.name; + const targetPath = join(baseDir, skillName); + + // Check if already exists + if (existsSync(targetPath)) { + return { error: `Skill '${skillName}' already exists in ${location} location` }; + } + + // Copy entire folder recursively + await copyDirectoryRecursive(sourcePath, targetPath); + + return { + success: true, + skillName, + location, + path: targetPath + }; + } catch (error) { + return { error: (error as Error).message }; + } +} + +/** + * Generate skill via CLI tool (Gemini) + * @param {Object} params - Generation parameters + * @param {string} params.generationType - 'description' or 'template' + * @param {string} params.description - Skill description from user + * @param {string} params.skillName - Name for the skill + * @param {string} params.location - 'project' or 'user' + * @param {string} params.projectPath - Project root path + * @returns {Object} + */ +async function generateSkillViaCLI({ generationType, description, skillName, location, projectPath }) { + try { + // Validate inputs + if (!skillName) { + return { error: 'Skill name is required' }; + } + if (generationType === 'description' && !description) { + return { error: 'Description is required for description-based generation' }; + } + + // Determine target directory + const baseDir = location === 'project' + ? join(projectPath, '.claude', 'skills') + : join(homedir(), '.claude', 'skills'); + + const targetPath = join(baseDir, skillName); + + // Check if already exists + if (existsSync(targetPath)) { + return { error: `Skill '${skillName}' already exists in ${location} location` }; + } + + // Ensure base directory exists + if (!existsSync(baseDir)) { + await fsPromises.mkdir(baseDir, { recursive: true }); + } + + // Build CLI prompt + const targetLocationDisplay = location === 'project' + ? '.claude/skills/' + : '~/.claude/skills/'; + + const prompt = `PURPOSE: Generate a complete Claude Code skill from description +TASK: • Parse skill requirements • Create SKILL.md with proper frontmatter (name, description, version, allowed-tools) • Generate supporting files if needed in skill folder +MODE: write +CONTEXT: @**/* +EXPECTED: Complete skill folder structure with SKILL.md and all necessary files +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) | Follow Claude Code skill format | Include name, description in frontmatter | write=CREATE + +SKILL DESCRIPTION: +${description || 'Generate a basic skill template'} + +SKILL NAME: ${skillName} +TARGET LOCATION: ${targetLocationDisplay} +TARGET PATH: ${targetPath} + +REQUIREMENTS: +1. Create SKILL.md with frontmatter containing: + - name: "${skillName}" + - description: Brief description of the skill + - version: "1.0.0" + - allowed-tools: List of tools this skill can use (e.g., [Read, Write, Edit, Bash]) +2. Add skill content below frontmatter explaining what the skill does and how to use it +3. If the skill requires supporting files (e.g., templates, scripts), create them in the skill folder +4. Ensure all files are properly formatted and follow best practices`; + + // Execute CLI tool (Gemini) with write mode + const result = await executeCliTool({ + tool: 'gemini', + prompt, + mode: 'write', + cd: baseDir, + timeout: 600000, // 10 minutes + category: 'internal' + }); + + // Check if execution was successful + if (!result.success) { + return { + error: `CLI generation failed: ${result.stderr || 'Unknown error'}`, + stdout: result.stdout, + stderr: result.stderr + }; + } + + // Validate the generated skill + const validation = validateSkillFolder(targetPath); + if (!validation.valid) { + return { + error: `Generated skill is invalid: ${validation.errors.join(', ')}`, + stdout: result.stdout, + stderr: result.stderr + }; + } + + return { + success: true, + skillName: validation.skillInfo.name, + location, + path: targetPath, + stdout: result.stdout, + stderr: result.stderr + }; + } catch (error) { + return { error: (error as Error).message }; + } +} + // ========== Skills API Routes ========== /** @@ -296,5 +541,59 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { return true; } + // API: Validate skill import + if (pathname === '/api/skills/validate-import' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { sourcePath } = body; + if (!sourcePath) { + return { valid: false, errors: ['Source path is required'], skillInfo: null }; + } + return validateSkillFolder(sourcePath); + }); + return true; + } + + // API: Create/Import skill + if (pathname === '/api/skills/create' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { mode, location, sourcePath, skillName, description, generationType, projectPath: projectPathParam } = body; + + if (!mode) { + return { error: 'Mode is required (import or cli-generate)' }; + } + + if (!location) { + return { error: 'Location is required (project or user)' }; + } + + const projectPath = projectPathParam || initialPath; + + if (mode === 'import') { + // Import mode: copy existing skill folder + if (!sourcePath) { + return { error: 'Source path is required for import mode' }; + } + + return await importSkill(sourcePath, location, projectPath, skillName); + } else if (mode === 'cli-generate') { + // CLI generate mode: use Gemini to generate skill + if (!skillName) { + return { error: 'Skill name is required for CLI generation mode' }; + } + + return await generateSkillViaCLI({ + generationType: generationType || 'description', + description, + skillName, + location, + projectPath + }); + } else { + return { error: 'Invalid mode. Must be "import" or "cli-generate"' }; + } + }); + return true; + } + return false; } diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index 674bf9e8..0d139691 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -17,6 +17,7 @@ import { handleSkillsRoutes } from './routes/skills-routes.js'; import { handleRulesRoutes } from './routes/rules-routes.js'; import { handleSessionRoutes } from './routes/session-routes.js'; import { handleCcwRoutes } from './routes/ccw-routes.js'; +import { handleClaudeRoutes } from './routes/claude-routes.js'; // Import WebSocket handling import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js'; @@ -59,7 +60,8 @@ const MODULE_CSS_FILES = [ '10-cli.css', '11-memory.css', '11-prompt-history.css', - '12-skills-rules.css' + '12-skills-rules.css', + '13-claude-manager.css' ]; // Modular JS files in dependency order @@ -104,6 +106,7 @@ const MODULE_FILES = [ 'views/prompt-history.js', 'views/skills-manager.js', 'views/rules-manager.js', + 'views/claude-manager.js', 'main.js' ]; @@ -241,6 +244,11 @@ export async function startServer(options: ServerOptions = {}): Promise div, +#ruleValidationResult > div { + animation: slideDown 0.2s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Path Input List Animations */ +#rulePathsList > div { + animation: slideDown 0.15s ease-out; +} + +/* Form Input Focus States */ +.modal-dialog input:focus, +.modal-dialog textarea:focus, +.modal-dialog select:focus { + outline: none; + border-color: hsl(var(--primary)); + ring: 2px; + ring-color: hsl(var(--primary) / 0.2); +} + +/* Checkbox Custom Styling */ +.modal-dialog input[type="checkbox"] { + cursor: pointer; + accent-color: hsl(var(--primary)); +} + +/* Textarea Specific */ +.modal-dialog textarea { + resize: vertical; + min-height: 100px; +} + +/* Button Hover States */ +.modal-dialog button:not(:disabled):hover { + opacity: 0.9; +} + +.modal-dialog button:not(:disabled):active { + transform: scale(0.98); +} + +/* Loading Spinner in Validation */ +.animate-spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Responsive Modal */ +@media (max-width: 640px) { + .modal-dialog { + max-width: 95vw; + max-height: 95vh; + } +} diff --git a/ccw/src/templates/dashboard-css/13-claude-manager.css b/ccw/src/templates/dashboard-css/13-claude-manager.css new file mode 100644 index 00000000..d0abc2b0 --- /dev/null +++ b/ccw/src/templates/dashboard-css/13-claude-manager.css @@ -0,0 +1,759 @@ +/* ======================================== + * CLAUDE.md Manager Styles + * Three-column layout: File Tree | Viewer/Editor | Metadata + * ======================================== */ + +/* ======================================== + * Main Layout + * ======================================== */ +.claude-manager-view { + height: 100%; + min-height: calc(100vh - 180px); + max-height: calc(100vh - 180px); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.claude-manager-view.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* ======================================== + * Header + * ======================================== */ +.claude-manager-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0; + margin-bottom: 0.5rem; + border-bottom: 1px solid hsl(var(--border)); + flex-shrink: 0; +} + +.claude-manager-header-left { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.claude-manager-header-left h2 { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.125rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin: 0; +} + +.file-count-badge { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + border-radius: 0.25rem; +} + +.claude-manager-header-right { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* ======================================== + * Three-Column Grid + * ======================================== */ +.claude-manager-columns { + display: grid; + grid-template-columns: 280px 1fr 320px; + gap: 1rem; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.claude-manager-column { + display: flex; + flex-direction: column; + min-height: 0; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; +} + +.claude-manager-column.left { + overflow-y: auto; + height: 100%; +} + +.claude-manager-column.center { + overflow: hidden; + height: 100%; +} + +.claude-manager-column.right { + overflow-y: auto; + height: 100%; +} + +/* ======================================== + * File Tree (Left Column) + * ======================================== */ +.file-tree { + padding: 0; +} + +/* Search Box */ +.file-tree-search { + position: relative; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid hsl(var(--border)); +} + +.file-tree-search input { + width: 100%; + padding: 0.5rem 2.5rem 0.5rem 0.75rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.875rem; +} + +.file-tree-search input:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1); +} + +.file-tree-search i { + position: absolute; + right: 1.5rem; + top: 50%; + transform: translateY(-50%); + color: hsl(var(--muted-foreground)); + pointer-events: none; +} + +.file-tree-section { + margin-bottom: 0.5rem; +} + +.file-tree-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + cursor: pointer; + font-weight: 600; + color: hsl(var(--foreground)); + border-radius: 0.25rem; + transition: background-color 0.15s; +} + +.file-tree-header:hover { + background: hsl(var(--muted)); +} + +.file-tree-header .file-count { + margin-left: auto; + font-size: 0.75rem; + padding: 0.125rem 0.375rem; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + border-radius: 0.25rem; +} + +.file-tree-subheader { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 0.5rem; + cursor: pointer; + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + border-radius: 0.25rem; + transition: background-color 0.15s; +} + +.file-tree-subheader:hover { + background: hsl(var(--muted) / 0.5); +} + +.file-tree-subheader .file-count { + margin-left: auto; + font-size: 0.7rem; + padding: 0.125rem 0.25rem; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + border-radius: 0.25rem; +} + +.file-tree-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 0.5rem; + cursor: pointer; + font-size: 0.875rem; + color: hsl(var(--foreground)); + border-radius: 0.25rem; + transition: background-color 0.15s; +} + +.file-tree-item:hover { + background: hsl(var(--muted) / 0.5); +} + +.file-tree-item.selected { + background: hsl(var(--primary) / 0.1); + color: hsl(var(--primary)); + font-weight: 500; +} + +.file-tree-item.empty { + color: hsl(var(--muted-foreground)); + cursor: default; + font-style: italic; +} + +.file-tree-item.empty:hover { + background: transparent; +} + +.file-tree-item .file-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-tree-item .file-path-hint { + font-size: 0.7rem; + color: hsl(var(--muted-foreground)); + opacity: 0.7; +} + +/* Color coding for level icons */ +.text-orange-500 { + color: hsl(25, 95%, 53%); +} + +.text-green-500 { + color: hsl(142, 71%, 45%); +} + +.text-blue-500 { + color: hsl(217, 91%, 60%); +} + +/* ======================================== + * File Viewer (Center Column) + * ======================================== */ +.file-viewer { + display: flex; + flex-direction: column; + height: 100%; +} + +.file-viewer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + border-bottom: 1px solid hsl(var(--border)); + flex-shrink: 0; +} + +.file-viewer-header h3 { + font-size: 1rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin: 0; +} + +.file-viewer-actions { + display: flex; + gap: 0.5rem; +} + +.file-viewer-content { + flex: 1; + overflow-y: auto; + padding: 1rem; +} + +.markdown-content { + line-height: 1.6; + color: hsl(var(--foreground)); +} + +.markdown-content h1 { + font-size: 1.5rem; + font-weight: 700; + margin: 1.5rem 0 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid hsl(var(--border)); +} + +.markdown-content h2 { + font-size: 1.25rem; + font-weight: 600; + margin: 1.25rem 0 0.75rem; +} + +.markdown-content h3 { + font-size: 1.125rem; + font-weight: 600; + margin: 1rem 0 0.5rem; +} + +.markdown-content strong { + font-weight: 600; + color: hsl(var(--foreground)); +} + +.markdown-content em { + font-style: italic; +} + +.markdown-content code { + font-family: 'Consolas', 'Monaco', monospace; + font-size: 0.875rem; + padding: 0.125rem 0.375rem; + background: hsl(var(--muted)); + border-radius: 0.25rem; +} + +.file-editor { + width: 100%; + height: 100%; + min-height: 400px; + padding: 1rem; + font-family: 'Consolas', 'Monaco', monospace; + font-size: 0.875rem; + line-height: 1.5; + color: hsl(var(--foreground)); + background: hsl(var(--background)); + border: none; + outline: none; + resize: none; +} + +.file-editor:focus { + background: hsl(var(--background)); +} + +/* ======================================== + * File Metadata (Right Column) + * ======================================== */ +.file-metadata { + padding: 1rem; +} + +.metadata-section { + margin-bottom: 1.5rem; +} + +.metadata-section h4 { + font-size: 0.875rem; + font-weight: 600; + color: hsl(var(--muted-foreground)); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0 0 0.75rem; +} + +.metadata-item { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-bottom: 0.75rem; +} + +.metadata-item .label { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.metadata-item .value { + font-size: 0.875rem; + color: hsl(var(--foreground)); + font-weight: 500; +} + +.metadata-item .value.path { + font-family: 'Consolas', 'Monaco', monospace; + font-size: 0.75rem; + word-break: break-all; + background: hsl(var(--muted)); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +.metadata-section .btn.full-width { + width: 100%; + margin-bottom: 0.5rem; + justify-content: center; +} + +/* ======================================== + * CLI Sync Panel + * ======================================== */ +.cli-sync-panel { + padding: 1rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + background: hsl(var(--card)); +} + +.cli-sync-panel .panel-header { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + margin-bottom: 1rem; + color: hsl(var(--primary)); +} + +.sync-config { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.sync-config label { + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--muted-foreground)); + margin-bottom: 0.25rem; +} + +.sync-config .sync-select, +.sync-config select { + padding: 0.5rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.875rem; + width: 100%; +} + +.sync-config .sync-select:focus, +.sync-config select:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1); +} + +.sync-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.625rem; + background: hsl(var(--primary)); + color: white; + border: none; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + transition: opacity 0.2s; +} + +.sync-button:hover:not(:disabled) { + opacity: 0.9; +} + +.sync-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.sync-progress { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem; + margin-top: 0.75rem; + background: hsl(var(--accent)); + border-radius: 0.375rem; + font-size: 0.875rem; + color: hsl(var(--accent-foreground)); +} + +.sync-progress i { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* ======================================== + * Empty State + * ======================================== */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: hsl(var(--muted-foreground)); + text-align: center; + padding: 2rem; +} + +.empty-state i { + margin-bottom: 1rem; +} + +.empty-state p { + margin: 0; + font-size: 0.875rem; +} + +/* ======================================== + * File Type Icons + * ======================================== */ +.file-icon { + flex-shrink: 0; +} + +.file-icon.directory { + color: hsl(var(--primary)); +} + +.file-icon.markdown { + color: hsl(25, 95%, 53%); +} + +.file-icon.code { + color: hsl(217, 91%, 60%); +} + +.file-icon.json { + color: hsl(142, 71%, 45%); +} + +.file-icon.css { + color: hsl(262, 83%, 58%); +} + +.file-size { + margin-left: auto; + font-size: 0.7rem; + color: hsl(var(--muted-foreground)); + opacity: 0.7; +} + +/* Search Highlight */ +.search-highlight { + background: hsl(45, 100%, 70%); + color: hsl(0, 0%, 10%); + font-weight: 600; + padding: 0.125rem 0.25rem; + border-radius: 0.125rem; +} + +/* ======================================== + * Create Dialog (Modal) + * ======================================== */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.2s; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.create-dialog { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + padding: 1.5rem; + width: 90%; + max-width: 500px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + animation: slideUp 0.3s; +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.create-dialog h3 { + margin: 0 0 1.5rem 0; + color: hsl(var(--foreground)); + font-size: 1.125rem; + font-weight: 600; +} + +.dialog-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.dialog-form label { + display: block; + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; + color: hsl(var(--muted-foreground)); +} + +.dialog-form input, +.dialog-form select { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.875rem; +} + +.dialog-form input:focus, +.dialog-form select:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1); +} + +.dialog-buttons { + display: flex; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.dialog-buttons button { + flex: 1; +} + +/* ======================================== + * Enhanced Markdown Content + * ======================================== */ +.markdown-content a { + color: hsl(var(--primary)); + text-decoration: underline; +} + +.markdown-content a:hover { + opacity: 0.8; +} + +.markdown-content table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; + font-size: 0.875rem; +} + +.markdown-content table th, +.markdown-content table td { + padding: 0.5rem 0.75rem; + border: 1px solid hsl(var(--border)); +} + +.markdown-content table th { + background: hsl(var(--muted)); + font-weight: 600; +} + +.markdown-content pre { + background: hsl(var(--muted)); + padding: 1rem; + border-radius: 0.375rem; + overflow-x: auto; + margin: 1rem 0; +} + +.markdown-content pre code { + background: transparent; + padding: 0; +} + +.markdown-content ul, +.markdown-content ol { + margin: 0.5rem 0; + padding-left: 1.5rem; +} + +.markdown-content li { + margin: 0.25rem 0; +} + +.task-list-item { + list-style: none; + margin-left: -1.5rem; +} + +.task-list-item input[type="checkbox"] { + margin-right: 0.5rem; +} + +/* ======================================== + * Button Styles + * ======================================== */ +.btn-danger { + background: hsl(0, 72%, 51%); + color: white; + border-color: hsl(0, 72%, 51%); +} + +.btn-danger:hover:not(:disabled) { + background: hsl(0, 72%, 45%); + border-color: hsl(0, 72%, 45%); +} + +/* ======================================== + * Responsive Design + * ======================================== */ +@media (max-width: 1400px) { + .claude-manager-columns { + grid-template-columns: 240px 1fr 280px; + } +} + +@media (max-width: 1024px) { + .claude-manager-columns { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto; + } + + .claude-manager-column.left { + max-height: 200px; + } + + .claude-manager-column.right { + max-height: 300px; + } +} diff --git a/ccw/src/templates/dashboard-js/components/cli-status.js b/ccw/src/templates/dashboard-js/components/cli-status.js index e6dbc6be..56778f75 100644 --- a/ccw/src/templates/dashboard-js/components/cli-status.js +++ b/ccw/src/templates/dashboard-js/components/cli-status.js @@ -184,6 +184,9 @@ function renderCliStatus() { ` : ` + ` } @@ -379,8 +382,121 @@ async function refreshAllCliStatus() { renderCliStatus(); } -async function installCodexLens() { - showRefreshToast('Installing CodexLens...', 'info'); +function installCodexLens() { + openCodexLensInstallWizard(); +} + +function openCodexLensInstallWizard() { + const modal = document.createElement('div'); + modal.id = 'codexlensInstallModal'; + modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; + modal.innerHTML = ` +
+
+
+
+ +
+
+

Install CodexLens

+

Python-based code indexing engine

+
+
+ +
+
+

What will be installed:

+
    +
  • + + Python virtual environment - Isolated Python environment +
  • +
  • + + CodexLens package - Code indexing and search engine +
  • +
  • + + SQLite FTS5 - Full-text search database +
  • +
+
+ +
+
+ +
+

Installation Location

+

~/.codexlens/venv

+

First installation may take 2-3 minutes to download and setup Python packages.

+
+
+
+ + +
+
+ +
+ + +
+
+ `; + + document.body.appendChild(modal); + + if (window.lucide) { + lucide.createIcons(); + } +} + +function closeCodexLensInstallWizard() { + const modal = document.getElementById('codexlensInstallModal'); + if (modal) { + modal.remove(); + } +} + +async function startCodexLensInstall() { + const progressDiv = document.getElementById('codexlensInstallProgress'); + const installBtn = document.getElementById('codexlensInstallBtn'); + const statusText = document.getElementById('codexlensInstallStatus'); + const progressBar = document.getElementById('codexlensProgressBar'); + + // Show progress, disable button + progressDiv.classList.remove('hidden'); + installBtn.disabled = true; + installBtn.innerHTML = 'Installing...'; + + // Simulate progress stages + const stages = [ + { progress: 10, text: 'Creating virtual environment...' }, + { progress: 30, text: 'Installing pip packages...' }, + { progress: 50, text: 'Installing CodexLens package...' }, + { progress: 70, text: 'Setting up Python dependencies...' }, + { progress: 90, text: 'Finalizing installation...' } + ]; + + let currentStage = 0; + const progressInterval = setInterval(() => { + if (currentStage < stages.length) { + statusText.textContent = stages[currentStage].text; + progressBar.style.width = `${stages[currentStage].progress}%`; + currentStage++; + } + }, 1500); try { const response = await fetch('/api/codexlens/bootstrap', { @@ -389,40 +505,288 @@ async function installCodexLens() { body: JSON.stringify({}) }); + clearInterval(progressInterval); const result = await response.json(); + if (result.success) { - showRefreshToast('CodexLens installed successfully!', 'success'); - await loadCodexLensStatus(); - renderCliStatus(); + progressBar.style.width = '100%'; + statusText.textContent = 'Installation complete!'; + + setTimeout(() => { + closeCodexLensInstallWizard(); + showRefreshToast('CodexLens installed successfully!', 'success'); + loadCodexLensStatus().then(() => renderCliStatus()); + }, 1000); } else { - showRefreshToast(`Install failed: ${result.error}`, 'error'); + statusText.textContent = `Error: ${result.error}`; + progressBar.classList.add('bg-destructive'); + installBtn.disabled = false; + installBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); } } catch (err) { - showRefreshToast(`Install error: ${err.message}`, 'error'); + clearInterval(progressInterval); + statusText.textContent = `Error: ${err.message}`; + progressBar.classList.add('bg-destructive'); + installBtn.disabled = false; + installBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); + } +} + +function uninstallCodexLens() { + openCodexLensUninstallWizard(); +} + +function openCodexLensUninstallWizard() { + const modal = document.createElement('div'); + modal.id = 'codexlensUninstallModal'; + modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; + modal.innerHTML = ` +
+
+
+
+ +
+
+

Uninstall CodexLens

+

Remove CodexLens and all data

+
+
+ +
+
+

What will be removed:

+
    +
  • + + Virtual environment at ~/.codexlens/venv +
  • +
  • + + All CodexLens indexed data and databases +
  • +
  • + + Configuration and semantic search models +
  • +
+
+ +
+
+ +
+

Warning

+

This action cannot be undone. All indexed code data will be permanently deleted.

+
+
+
+ + +
+
+ +
+ + +
+
+ `; + + document.body.appendChild(modal); + + if (window.lucide) { + lucide.createIcons(); + } +} + +function closeCodexLensUninstallWizard() { + const modal = document.getElementById('codexlensUninstallModal'); + if (modal) { + modal.remove(); + } +} + +async function startCodexLensUninstall() { + const progressDiv = document.getElementById('codexlensUninstallProgress'); + const uninstallBtn = document.getElementById('codexlensUninstallBtn'); + const statusText = document.getElementById('codexlensUninstallStatus'); + const progressBar = document.getElementById('codexlensUninstallProgressBar'); + + // Show progress, disable button + progressDiv.classList.remove('hidden'); + uninstallBtn.disabled = true; + uninstallBtn.innerHTML = 'Uninstalling...'; + + // Simulate progress stages + const stages = [ + { progress: 25, text: 'Removing virtual environment...' }, + { progress: 50, text: 'Deleting indexed data...' }, + { progress: 75, text: 'Cleaning up configuration...' }, + { progress: 90, text: 'Finalizing removal...' } + ]; + + let currentStage = 0; + const progressInterval = setInterval(() => { + if (currentStage < stages.length) { + statusText.textContent = stages[currentStage].text; + progressBar.style.width = `${stages[currentStage].progress}%`; + currentStage++; + } + }, 500); + + try { + const response = await fetch('/api/codexlens/uninstall', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}) + }); + + clearInterval(progressInterval); + const result = await response.json(); + + if (result.success) { + progressBar.style.width = '100%'; + statusText.textContent = 'Uninstallation complete!'; + + setTimeout(() => { + closeCodexLensUninstallWizard(); + showRefreshToast('CodexLens uninstalled successfully!', 'success'); + loadCodexLensStatus().then(() => renderCliStatus()); + }, 1000); + } else { + statusText.textContent = `Error: ${result.error}`; + progressBar.classList.remove('bg-destructive'); + progressBar.classList.add('bg-destructive'); + uninstallBtn.disabled = false; + uninstallBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); + } + } catch (err) { + clearInterval(progressInterval); + statusText.textContent = `Error: ${err.message}`; + progressBar.classList.remove('bg-destructive'); + progressBar.classList.add('bg-destructive'); + uninstallBtn.disabled = false; + uninstallBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); } } async function initCodexLensIndex() { + // Get current workspace path from multiple sources + let targetPath = null; + + // Helper function to check if path is valid + const isValidPath = (path) => { + return path && typeof path === 'string' && path.length > 0 && + (path.includes('/') || path.includes('\\')) && + !path.startsWith('{{') && !path.endsWith('}}'); + }; + + console.log('[CodexLens] Attempting to get project path...'); + + // Try 1: Global projectPath variable + if (isValidPath(projectPath)) { + targetPath = projectPath; + console.log('[CodexLens] ✓ Using global projectPath:', targetPath); + } + + // Try 2: Get from workflowData + if (!targetPath && typeof workflowData !== 'undefined' && workflowData && isValidPath(workflowData.projectPath)) { + targetPath = workflowData.projectPath; + console.log('[CodexLens] ✓ Using workflowData.projectPath:', targetPath); + } + + // Try 3: Get from current path display element + if (!targetPath) { + const currentPathEl = document.getElementById('currentPath'); + if (currentPathEl && currentPathEl.textContent) { + const pathText = currentPathEl.textContent.trim(); + if (isValidPath(pathText)) { + targetPath = pathText; + console.log('[CodexLens] ✓ Using currentPath element text:', targetPath); + } + } + } + + // Final validation + if (!targetPath) { + showRefreshToast('Error: No workspace loaded. Please open a workspace first.', 'error'); + console.error('[CodexLens] No valid project path available'); + console.error('[CodexLens] Attempted sources: projectPath:', projectPath, 'workflowData:', workflowData); + return; + } + showRefreshToast('Initializing CodexLens index...', 'info'); + console.log('[CodexLens] Initializing index for path:', targetPath); try { const response = await fetch('/api/codexlens/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: projectPath }) + body: JSON.stringify({ path: targetPath }) }); const result = await response.json(); + console.log('[CodexLens] Init result:', result); + if (result.success) { - const data = result.result?.result || result.result || result; + let data = null; + + // Try to parse nested JSON in output field + if (result.output && typeof result.output === 'string') { + try { + // Extract JSON from output (it may contain other text before the JSON) + const jsonMatch = result.output.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + data = parsed.result || parsed; + console.log('[CodexLens] Parsed from output:', data); + } + } catch (e) { + console.warn('[CodexLens] Failed to parse output as JSON:', e); + } + } + + // Fallback to direct result field + if (!data) { + data = result.result?.result || result.result || result; + } + const files = data.files_indexed || 0; + const dirs = data.dirs_indexed || 0; const symbols = data.symbols_indexed || 0; - showRefreshToast(`Index created: ${files} files, ${symbols} symbols`, 'success'); + + console.log('[CodexLens] Parsed data:', { files, dirs, symbols }); + + if (files === 0 && dirs === 0) { + showRefreshToast(`Warning: No files indexed. Path: ${targetPath}`, 'warning'); + console.warn('[CodexLens] No files indexed. Full data:', data); + } else { + showRefreshToast(`Index created: ${files} files, ${dirs} directories`, 'success'); + console.log('[CodexLens] Index created successfully'); + } } else { showRefreshToast(`Init failed: ${result.error}`, 'error'); + console.error('[CodexLens] Init error:', result.error); } } catch (err) { showRefreshToast(`Init error: ${err.message}`, 'error'); + console.error('[CodexLens] Exception:', err); } } @@ -591,7 +955,7 @@ function openSemanticSettingsModal() { tool.charAt(0).toUpperCase() + tool.slice(1) + ''; }).join(''); - const fallbackOptions = '' + availableTools.map(function(tool) { + const fallbackOptions = '' + availableTools.map(function(tool) { return ''; }).join(''); @@ -607,16 +971,16 @@ function openSemanticSettingsModal() { '' + '' + '
' + - '

Semantic Search Settings

' + - '

Configure LLM enhancement for semantic indexing

' + + '

' + t('semantic.settings') + '

' + + '

' + t('semantic.configDesc') + '

' + '
' + '' + '
' + '
' + '
' + '

' + - 'LLM Enhancement

' + - '

Use LLM to generate code summaries for better semantic search

' + + '' + t('semantic.llmEnhancement') + '' + + '

' + t('semantic.llmDesc') + '

' + '
' + '
' + '
' + '
' + '' + + '' + t('semantic.batchSize') + '' + '' + '
' + '
' + '' + + '' + t('semantic.timeout') + '' + '' + + '
' + + '
' + + '' + + '
' + + '' + + '
' + '
' + '' + '' + '
' + - '' + + '' + '
' + ''; document.body.appendChild(modal); + // Add semantic search button handler + setTimeout(function() { + var runSemanticSearchBtn = document.getElementById('runSemanticSearchBtn'); + if (runSemanticSearchBtn) { + runSemanticSearchBtn.onclick = async function() { + var query = document.getElementById('semanticSearchInput').value.trim(); + var resultsDiv = document.getElementById('semanticSearchResults'); + var resultCount = document.getElementById('semanticResultCount'); + var resultContent = document.getElementById('semanticResultContent'); + + if (!query) { + showRefreshToast(t('codexlens.enterQuery'), 'warning'); + return; + } + + runSemanticSearchBtn.disabled = true; + runSemanticSearchBtn.innerHTML = '' + t('codexlens.searching') + ''; + resultsDiv.classList.add('hidden'); + + try { + var params = new URLSearchParams({ + query: query, + mode: 'semantic', + limit: '10' + }); + + var response = await fetch('/api/codexlens/search?' + params.toString()); + var result = await response.json(); + + console.log('[Semantic Search Test] Result:', result); + + if (result.success) { + var results = result.results || []; + resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount'); + resultContent.textContent = JSON.stringify(results, null, 2); + resultsDiv.classList.remove('hidden'); + showRefreshToast(t('codexlens.searchCompleted') + ': ' + results.length + ' ' + t('codexlens.resultsCount'), 'success'); + } else { + resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError')); + resultsDiv.classList.remove('hidden'); + showRefreshToast(t('codexlens.searchFailed') + ': ' + result.error, 'error'); + } + + runSemanticSearchBtn.disabled = false; + runSemanticSearchBtn.innerHTML = ' ' + t('semantic.runSearch'); + if (window.lucide) lucide.createIcons(); + } catch (err) { + console.error('[Semantic Search Test] Error:', err); + resultContent.textContent = t('common.exception') + ': ' + err.message; + resultsDiv.classList.remove('hidden'); + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + runSemanticSearchBtn.disabled = false; + runSemanticSearchBtn.innerHTML = ' ' + t('semantic.runSearch'); + if (window.lucide) lucide.createIcons(); + } + }; + } + }, 100); + var handleEscape = function(e) { if (e.key === 'Escape') { closeSemanticSettingsModal(); @@ -713,37 +1161,37 @@ function toggleLlmEnhancement(enabled) { } renderCliStatus(); - showRefreshToast('LLM Enhancement ' + (enabled ? 'enabled' : 'disabled'), 'success'); + showRefreshToast(t('semantic.llmEnhancement') + ' ' + (enabled ? t('semantic.enabled') : t('semantic.disabled')), 'success'); } function updateLlmTool(tool) { llmEnhancementSettings.tool = tool; localStorage.setItem('ccw-llm-enhancement-tool', tool); - showRefreshToast('Primary LLM tool set to ' + tool, 'success'); + showRefreshToast(t('semantic.toolSetTo') + ' ' + tool, 'success'); } function updateLlmFallback(tool) { llmEnhancementSettings.fallbackTool = tool; localStorage.setItem('ccw-llm-enhancement-fallback', tool); - showRefreshToast('Fallback tool set to ' + (tool || 'none'), 'success'); + showRefreshToast(t('semantic.fallbackSetTo') + ' ' + (tool || t('semantic.none')), 'success'); } function updateLlmBatchSize(size) { llmEnhancementSettings.batchSize = parseInt(size, 10); localStorage.setItem('ccw-llm-enhancement-batch-size', size); - showRefreshToast('Batch size set to ' + size + ' files', 'success'); + showRefreshToast(t('semantic.batchSetTo') + ' ' + size + ' ' + t('semantic.files'), 'success'); } function updateLlmTimeout(ms) { llmEnhancementSettings.timeoutMs = parseInt(ms, 10); localStorage.setItem('ccw-llm-enhancement-timeout', ms); var mins = parseInt(ms, 10) / 60000; - showRefreshToast('Timeout set to ' + mins + ' minute' + (mins > 1 ? 's' : ''), 'success'); + showRefreshToast(t('semantic.timeoutSetTo') + ' ' + mins + ' ' + (mins > 1 ? t('semantic.minutes') : t('semantic.minute')), 'success'); } async function runEnhanceCommand() { if (!llmEnhancementSettings.enabled) { - showRefreshToast('Please enable LLM Enhancement first', 'warning'); + showRefreshToast(t('semantic.enableFirst'), 'warning'); return; } diff --git a/ccw/src/templates/dashboard-js/components/mcp-manager.js b/ccw/src/templates/dashboard-js/components/mcp-manager.js index d93da030..abf753cf 100644 --- a/ccw/src/templates/dashboard-js/components/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/components/mcp-manager.js @@ -142,6 +142,59 @@ async function removeMcpServerFromProject(serverName) { } } +async function addGlobalMcpServer(serverName, serverConfig) { + try { + const response = await fetch('/api/mcp-add-global-server', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + serverName: serverName, + serverConfig: serverConfig + }) + }); + + if (!response.ok) throw new Error('Failed to add global MCP server'); + + const result = await response.json(); + if (result.success) { + await loadMcpConfig(); + renderMcpManager(); + showRefreshToast(`Global MCP server "${serverName}" added`, 'success'); + } + return result; + } catch (err) { + console.error('Failed to add global MCP server:', err); + showRefreshToast(`Failed to add global MCP server: ${err.message}`, 'error'); + return null; + } +} + +async function removeGlobalMcpServer(serverName) { + try { + const response = await fetch('/api/mcp-remove-global-server', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + serverName: serverName + }) + }); + + if (!response.ok) throw new Error('Failed to remove global MCP server'); + + const result = await response.json(); + if (result.success) { + await loadMcpConfig(); + renderMcpManager(); + showRefreshToast(`Global MCP server "${serverName}" removed`, 'success'); + } + return result; + } catch (err) { + console.error('Failed to remove global MCP server:', err); + showRefreshToast(`Failed to remove global MCP server: ${err.message}`, 'error'); + return null; + } +} + // ========== Badge Update ========== function updateMcpBadge() { const badge = document.getElementById('badgeMcpServers'); @@ -260,8 +313,40 @@ function isServerInCurrentProject(serverName) { return serverName in servers; } +// Generate install command for MCP server +function generateMcpInstallCommand(serverName, serverConfig, scope = 'project') { + const command = serverConfig.command || ''; + const args = serverConfig.args || []; + + // Check if it's an npx-based package + if (command === 'npx' && args.length > 0) { + const packageName = args[0]; + // Check if it's a scoped package or standard package + if (packageName.startsWith('@') || packageName.includes('/')) { + const scopeFlag = scope === 'global' ? ' --global' : ''; + return `claude mcp add ${packageName}${scopeFlag}`; + } + } + + // For custom servers, return JSON configuration + const scopeFlag = scope === 'global' ? ' --global' : ''; + return `claude mcp add ${serverName}${scopeFlag}`; +} + +// Copy install command to clipboard +async function copyMcpInstallCommand(serverName, serverConfig, scope = 'project') { + try { + const command = generateMcpInstallCommand(serverName, serverConfig, scope); + await navigator.clipboard.writeText(command); + showRefreshToast(t('mcp.installCmdCopied'), 'success'); + } catch (error) { + console.error('Failed to copy install command:', error); + showRefreshToast(t('mcp.installCmdFailed'), 'error'); + } +} + // ========== MCP Create Modal ========== -function openMcpCreateModal() { +function openMcpCreateModal(scope = 'project') { const modal = document.getElementById('mcpCreateModal'); if (modal) { modal.classList.remove('hidden'); @@ -276,6 +361,11 @@ function openMcpCreateModal() { // Clear JSON input document.getElementById('mcpServerJson').value = ''; document.getElementById('mcpJsonPreview').classList.add('hidden'); + // Set scope (global or project) + const scopeSelect = document.getElementById('mcpServerScope'); + if (scopeSelect) { + scopeSelect.value = scope; + } // Focus on name input document.getElementById('mcpServerName').focus(); // Setup JSON input listener @@ -427,6 +517,8 @@ async function submitMcpCreateFromForm() { const command = document.getElementById('mcpServerCommand').value.trim(); const argsText = document.getElementById('mcpServerArgs').value.trim(); const envText = document.getElementById('mcpServerEnv').value.trim(); + const scopeSelect = document.getElementById('mcpServerScope'); + const scope = scopeSelect ? scopeSelect.value : 'project'; // Validate required fields if (!name) { @@ -471,7 +563,7 @@ async function submitMcpCreateFromForm() { serverConfig.env = env; } - await createMcpServerWithConfig(name, serverConfig); + await createMcpServerWithConfig(name, serverConfig, scope); } async function submitMcpCreateFromJson() { @@ -550,18 +642,30 @@ async function submitMcpCreateFromJson() { } } -async function createMcpServerWithConfig(name, serverConfig) { +async function createMcpServerWithConfig(name, serverConfig, scope = 'project') { // Submit to API try { - const response = await fetch('/api/mcp-copy-server', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - projectPath: projectPath, - serverName: name, - serverConfig: serverConfig - }) - }); + let response; + if (scope === 'global') { + response = await fetch('/api/mcp-add-global-server', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + serverName: name, + serverConfig: serverConfig + }) + }); + } else { + response = await fetch('/api/mcp-copy-server', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + projectPath: projectPath, + serverName: name, + serverConfig: serverConfig + }) + }); + } if (!response.ok) throw new Error('Failed to create MCP server'); @@ -570,7 +674,8 @@ async function createMcpServerWithConfig(name, serverConfig) { closeMcpCreateModal(); await loadMcpConfig(); renderMcpManager(); - showRefreshToast(`MCP server "${name}" created successfully`, 'success'); + const scopeLabel = scope === 'global' ? 'global' : 'project'; + showRefreshToast(`MCP server "${name}" created in ${scopeLabel} scope`, 'success'); } else { showRefreshToast(result.error || 'Failed to create MCP server', 'error'); } diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js index 9ad921a1..c4ad7b95 100644 --- a/ccw/src/templates/dashboard-js/components/navigation.js +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -112,6 +112,8 @@ function initNavigation() { renderSkillsManager(); } else if (currentView === 'rules-manager') { renderRulesManager(); + } else if (currentView === 'claude-manager') { + renderClaudeManager(); } }); }); @@ -144,6 +146,8 @@ function updateContentTitle() { titleEl.textContent = t('title.skillsManager'); } else if (currentView === 'rules-manager') { titleEl.textContent = t('title.rulesManager'); + } else if (currentView === 'claude-manager') { + titleEl.textContent = t('title.claudeManager'); } else if (currentView === 'liteTasks') { const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') }; titleEl.textContent = names[currentLiteType] || t('title.liteTasks'); diff --git a/ccw/src/templates/dashboard-js/components/notifications.js b/ccw/src/templates/dashboard-js/components/notifications.js index 72c7ca29..bce88053 100644 --- a/ccw/src/templates/dashboard-js/components/notifications.js +++ b/ccw/src/templates/dashboard-js/components/notifications.js @@ -214,13 +214,13 @@ function handleNotification(data) { if (typeof handleMemoryUpdated === 'function') { handleMemoryUpdated(payload); } - // Force refresh of memory view if active - if (getCurrentView && getCurrentView() === 'memory') { - if (typeof loadMemoryStats === 'function') { - loadMemoryStats().then(function() { - if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn(); - }); - } + // Force refresh of memory view + if (typeof loadMemoryStats === 'function') { + loadMemoryStats().then(function() { + if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn(); + }).catch(function(err) { + console.error('[Memory] Failed to refresh stats:', err); + }); } break; @@ -254,15 +254,142 @@ function handleNotification(data) { 'Memory' ); } - // Refresh Active Memory status if on memory view - if (getCurrentView && getCurrentView() === 'memory') { - if (typeof loadActiveMemoryStatus === 'function') { - loadActiveMemoryStatus(); - } + // Refresh Active Memory status + if (typeof loadActiveMemoryStatus === 'function') { + loadActiveMemoryStatus().catch(function(err) { + console.error('[Active Memory] Failed to refresh status:', err); + }); } console.log('[Active Memory] Sync completed:', payload); break; + case 'CLAUDE_FILE_SYNCED': + // Handle CLAUDE.md file sync completion + if (typeof addGlobalNotification === 'function') { + const { path, level, tool, mode } = payload; + const fileName = path.split(/[/\\]/).pop(); + addGlobalNotification( + 'success', + `${fileName} synced`, + { + 'Level': level, + 'Tool': tool, + 'Mode': mode, + 'Time': new Date(payload.timestamp).toLocaleTimeString() + }, + 'CLAUDE.md' + ); + } + // Refresh file list + if (typeof loadClaudeFiles === 'function') { + loadClaudeFiles().then(() => { + // Re-render the view to show updated content + if (typeof renderClaudeManager === 'function') { + renderClaudeManager(); + } + }).catch(err => console.error('[CLAUDE.md] Failed to refresh files:', err)); + } + console.log('[CLAUDE.md] Sync completed:', payload); + break; + + case 'CLI_TOOL_INSTALLED': + // Handle CLI tool installation completion + if (typeof addGlobalNotification === 'function') { + const { tool } = payload; + addGlobalNotification( + 'success', + `${tool} installed successfully`, + { + 'Tool': tool, + 'Time': new Date(payload.timestamp).toLocaleTimeString() + }, + 'CLI Tools' + ); + } + // Refresh CLI manager + if (typeof loadCliToolStatus === 'function') { + loadCliToolStatus().then(() => { + if (typeof renderToolsSection === 'function') { + renderToolsSection(); + } + }).catch(err => console.error('[CLI Tools] Failed to refresh status:', err)); + } + console.log('[CLI Tools] Installation completed:', payload); + break; + + case 'CLI_TOOL_UNINSTALLED': + // Handle CLI tool uninstallation completion + if (typeof addGlobalNotification === 'function') { + const { tool } = payload; + addGlobalNotification( + 'success', + `${tool} uninstalled successfully`, + { + 'Tool': tool, + 'Time': new Date(payload.timestamp).toLocaleTimeString() + }, + 'CLI Tools' + ); + } + // Refresh CLI manager + if (typeof loadCliToolStatus === 'function') { + loadCliToolStatus().then(() => { + if (typeof renderToolsSection === 'function') { + renderToolsSection(); + } + }).catch(err => console.error('[CLI Tools] Failed to refresh status:', err)); + } + console.log('[CLI Tools] Uninstallation completed:', payload); + break; + + case 'CODEXLENS_INSTALLED': + // Handle CodexLens installation completion + if (typeof addGlobalNotification === 'function') { + const { version } = payload; + addGlobalNotification( + 'success', + `CodexLens installed successfully`, + { + 'Version': version || 'latest', + 'Time': new Date(payload.timestamp).toLocaleTimeString() + }, + 'CodexLens' + ); + } + // Refresh CLI status if active + if (typeof loadCodexLensStatus === 'function') { + loadCodexLensStatus().then(() => { + if (typeof renderCliStatus === 'function') { + renderCliStatus(); + } + }); + } + console.log('[CodexLens] Installation completed:', payload); + break; + + case 'CODEXLENS_UNINSTALLED': + // Handle CodexLens uninstallation completion + if (typeof addGlobalNotification === 'function') { + addGlobalNotification( + 'success', + `CodexLens uninstalled successfully`, + { + 'Time': new Date(payload.timestamp).toLocaleTimeString() + }, + 'CodexLens' + ); + } + // Refresh CLI status if active + if (typeof loadCodexLensStatus === 'function') { + loadCodexLensStatus().then(() => { + if (typeof renderCliStatus === 'function') { + renderCliStatus(); + } + }); + } + console.log('[CodexLens] Uninstallation completed:', payload); + break; + default: console.log('[WS] Unknown notification type:', type); } diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 126a1d09..a263252d 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -107,6 +107,8 @@ const i18n = { 'empty.noLiteSessionsText': 'No sessions found in .workflow/.{type}/', 'empty.noMcpServers': 'No MCP servers configured for this project', 'empty.addMcpServersHint': 'Add servers from the available list below', + 'empty.noGlobalMcpServers': 'No global MCP servers configured', + 'empty.globalServersHint': 'Global servers are available to all projects from ~/.claude.json', 'empty.noAdditionalMcp': 'No additional MCP servers found in other projects', 'empty.noHooks': 'No hooks configured for this project', 'empty.createHookHint': 'Create a hook to automate actions on tool usage', @@ -197,6 +199,7 @@ const i18n = { 'cli.setDefault': 'Set Default', 'cli.default': 'Default', 'cli.install': 'Install', + 'cli.uninstall': 'Uninstall', 'cli.initIndex': 'Init Index', 'cli.geminiDesc': 'Google AI for code analysis', 'cli.qwenDesc': 'Alibaba AI assistant', @@ -205,6 +208,80 @@ const i18n = { 'cli.codexLensDescFull': 'Full-text code search engine', 'cli.semanticDesc': 'AI-powered code understanding', 'cli.semanticDescFull': 'Natural language code search', + + // CodexLens Configuration + 'codexlens.config': 'CodexLens Configuration', + 'codexlens.status': 'Status', + 'codexlens.installed': 'Installed', + 'codexlens.notInstalled': 'Not Installed', + 'codexlens.indexes': 'Indexes', + 'codexlens.currentWorkspace': 'Current Workspace', + 'codexlens.indexStoragePath': 'Index Storage Path', + 'codexlens.whereIndexesStored': 'where indexes are stored', + 'codexlens.currentPath': 'Current Path', + 'codexlens.newStoragePath': 'New Storage Path', + 'codexlens.pathPlaceholder': 'e.g., /path/to/indexes or ~/.codexlens/indexes', + 'codexlens.pathInfo': 'Supports ~ for home directory. Changes take effect immediately.', + 'codexlens.migrationRequired': 'Migration Required', + 'codexlens.migrationWarning': 'After changing the path, existing indexes will need to be re-initialized for each workspace.', + 'codexlens.actions': 'Actions', + 'codexlens.initializeIndex': 'Initialize Index', + 'codexlens.cleanAllIndexes': 'Clean All Indexes', + 'codexlens.installCodexLens': 'Install CodexLens', + 'codexlens.testSearch': 'Test Search', + 'codexlens.testFunctionality': 'test CodexLens functionality', + 'codexlens.textSearch': 'Text Search', + 'codexlens.fileSearch': 'File Search', + 'codexlens.symbolSearch': 'Symbol Search', + 'codexlens.searchPlaceholder': 'Enter search query (e.g., function name, file path, code snippet)', + 'codexlens.runSearch': 'Run Search', + 'codexlens.results': 'Results', + 'codexlens.resultsCount': 'results', + 'codexlens.saveConfig': 'Save Configuration', + 'codexlens.searching': 'Searching...', + 'codexlens.searchCompleted': 'Search completed', + 'codexlens.searchFailed': 'Search failed', + 'codexlens.enterQuery': 'Please enter a search query', + 'codexlens.configSaved': 'Configuration saved successfully', + 'codexlens.pathEmpty': 'Index directory path cannot be empty', + 'codexlens.cleanConfirm': 'Are you sure you want to clean all CodexLens indexes? This cannot be undone.', + 'codexlens.cleaning': 'Cleaning indexes...', + 'codexlens.cleanSuccess': 'All indexes cleaned successfully', + 'codexlens.cleanFailed': 'Failed to clean indexes', + 'codexlens.loadingConfig': 'Loading configuration...', + + // Semantic Search Configuration + 'semantic.settings': 'Semantic Search Settings', + 'semantic.configDesc': 'Configure LLM enhancement for semantic indexing', + 'semantic.llmEnhancement': 'LLM Enhancement', + 'semantic.llmDesc': 'Use LLM to generate code summaries for better semantic search', + 'semantic.primaryTool': 'Primary LLM Tool', + 'semantic.fallbackTool': 'Fallback Tool', + 'semantic.batchSize': 'Batch Size', + 'semantic.timeout': 'Timeout', + 'semantic.file': 'file', + 'semantic.files': 'files', + 'semantic.enhanceInfo': 'LLM enhancement generates code summaries and keywords for each file, improving semantic search accuracy.', + 'semantic.enhanceCommand': 'Run', + 'semantic.enhanceAfterEnable': 'after enabling to process existing files.', + 'semantic.runEnhanceNow': 'Run Enhance Now', + 'semantic.viewStatus': 'View Status', + 'semantic.testSearch': 'Test Semantic Search', + 'semantic.searchPlaceholder': 'Enter semantic query (e.g., authentication logic, error handling)', + 'semantic.runSearch': 'Run Semantic Search', + 'semantic.close': 'Close', + 'semantic.enabled': 'enabled', + 'semantic.disabled': 'disabled', + 'semantic.toolSetTo': 'Primary LLM tool set to', + 'semantic.fallbackSetTo': 'Fallback tool set to', + 'semantic.none': 'none', + 'semantic.llmEnhancement': 'LLM Enhancement', + 'semantic.batchSetTo': 'Batch size set to', + 'semantic.timeoutSetTo': 'Timeout set to', + 'semantic.minute': 'minute', + 'semantic.minutes': 'minutes', + 'semantic.enableFirst': 'Please enable LLM Enhancement first', + 'cli.settings': 'CLI Execution Settings', 'cli.promptFormat': 'Prompt Format', 'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation', @@ -296,9 +373,18 @@ const i18n = { 'updateClaudeMd.cancel': 'Cancel', // MCP Manager - 'mcp.currentProject': 'Current Project MCP Servers', + 'mcp.currentAvailable': 'Current Available MCP', + 'mcp.projectAvailable': 'Current Available MCP', + 'mcp.newProjectServer': 'New Project Server', 'mcp.newServer': 'New Server', + 'mcp.newGlobalServer': 'New Global Server', + 'mcp.copyInstallCmd': 'Copy Install Command', + 'mcp.installCmdCopied': 'Install command copied to clipboard', + 'mcp.installCmdFailed': 'Failed to copy install command', 'mcp.serversConfigured': 'servers configured', + 'mcp.serversAvailable': 'servers available', + 'mcp.globalAvailable': '全局可用 MCP', + 'mcp.globalServersFrom': '个服务器来自 ~/.claude.json', 'mcp.enterprise': 'Enterprise MCP Servers', 'mcp.enterpriseManaged': 'Managed', 'mcp.enterpriseReadOnly': 'servers (read-only)', @@ -315,21 +401,28 @@ const i18n = { 'mcp.noMcpServers': 'No MCP servers', 'mcp.add': 'Add', 'mcp.addToProject': 'Add to Project', + 'mcp.installToProject': 'Install to project', + 'mcp.installToGlobal': 'Install to global', 'mcp.removeFromProject': 'Remove from project', 'mcp.removeConfirm': 'Remove MCP server "{name}" from this project?', + 'mcp.removeGlobal': 'Remove from global scope', + 'mcp.removeGlobalConfirm': 'Remove global MCP server "{name}"? This will affect all projects.', 'mcp.readOnly': 'Read-only', 'mcp.usedIn': 'Used in {count} project', 'mcp.usedInPlural': 'Used in {count} projects', 'mcp.availableToAll': 'Available to all projects from ~/.claude.json', 'mcp.managedByOrg': 'Managed by organization (highest priority)', 'mcp.variables': 'variables', - + // MCP Create Modal 'mcp.createTitle': 'Create MCP Server', 'mcp.form': 'Form', 'mcp.json': 'JSON', 'mcp.serverName': 'Server Name', 'mcp.serverNamePlaceholder': 'e.g., my-mcp-server', + 'mcp.scope': 'Scope', + 'mcp.scopeProject': 'Project - Only this project', + 'mcp.scopeGlobal': 'Global - All projects (~/.claude.json)', 'mcp.command': 'Command', 'mcp.commandPlaceholder': 'e.g., npx, uvx, node, python', 'mcp.arguments': 'Arguments (one per line)', @@ -653,6 +746,30 @@ const i18n = { 'skills.deleteError': 'Failed to delete skill', 'skills.editNotImplemented': 'Edit feature coming soon', 'skills.createNotImplemented': 'Create feature coming soon', + 'skills.createSkill': 'Create Skill', 'skills.sourceFolder': 'Source Folder', 'skills.sourceFolderPlaceholder': 'Path to skill folder', 'skills.sourceFolderHint': 'Select a folder containing a valid SKILL.md file', 'skills.sourceFolderRequired': 'Source folder path is required', 'skills.customName': 'Custom Name', 'skills.customNamePlaceholder': 'Leave empty to use skill name from SKILL.md', 'skills.customNameHint': 'Optional: Override the skill name', 'skills.validate': 'Validate', 'skills.import': 'Import', 'skills.validating': 'Validating...', 'skills.validSkill': 'Valid Skill', 'skills.invalidSkill': 'Invalid Skill', 'skills.validateFirst': 'Please validate the skill folder first', 'skills.created': 'Skill "{name}" created successfully', 'skills.createError': 'Failed to create skill', 'skills.validationError': 'Validation failed', 'skills.enterFolderPath': 'Enter skill folder path', 'skills.name': 'Name', + 'skills.createMode': 'Creation Mode', + 'skills.importFolder': 'Import Folder', + 'skills.importFolderHint': 'Import existing skill folder', + 'skills.cliGenerate': 'CLI Generate', + 'skills.cliGenerateHint': 'Generate using AI', + 'skills.generationType': 'Generation Type', + 'skills.fromDescription': 'From Description', + 'skills.fromDescriptionHint': 'Describe what you need', + 'skills.fromTemplate': 'From Template', + 'skills.comingSoon': 'Coming soon', + 'skills.skillName': 'Skill Name', + 'skills.skillNamePlaceholder': 'e.g., code-review, testing-helper', + 'skills.skillNameHint': 'Choose a descriptive name for the skill', + 'skills.skillNameRequired': 'Skill name is required', + 'skills.descriptionPlaceholder': 'Describe what this skill should help with...\nExample: Help review code for security vulnerabilities and best practices', + 'skills.descriptionRequired': 'Description is required', + 'skills.descriptionGenerateHint': 'Be specific about what the skill should do', + 'skills.generating': 'Generating skill via CLI... This may take a few minutes.', + 'skills.generated': 'Skill "{name}" generated successfully', + 'skills.generateError': 'Failed to generate skill', + 'skills.generate': 'Generate', + 'skills.cliGenerateInfo': 'AI will generate a complete skill based on your description', + 'skills.cliGenerateTimeHint': 'Generation may take a few minutes depending on complexity', // Rules 'nav.rules': 'Rules', @@ -679,9 +796,116 @@ const i18n = { 'rules.deleteError': 'Failed to delete rule', 'rules.editNotImplemented': 'Edit feature coming soon', 'rules.createNotImplemented': 'Create feature coming soon', + 'rules.createRule': 'Create Rule', + 'rules.location': 'Location', + 'rules.fileName': 'File Name', + 'rules.fileNameHint': 'Must end with .md', + 'rules.fileNameRequired': 'File name is required', + 'rules.fileNameMustEndMd': 'File name must end with .md', + 'rules.subdirectory': 'Subdirectory', + 'rules.subdirectoryHint': 'Optional: Organize rules into subdirectories', + 'rules.conditionalRule': 'Conditional Rule', + 'rules.conditionalHint': 'Apply this rule only to specific file paths', + 'rules.addPath': 'Add Path', + 'rules.contentPlaceholder': 'Enter rule content in Markdown format...', + 'rules.contentHint': 'Use Markdown to write rule instructions for Claude', + 'rules.contentRequired': 'Content is required', + 'rules.created': 'Rule "{name}" created successfully', + 'rules.createError': 'Failed to create rule', + 'rules.createMode': 'Creation Mode', + 'rules.manualInput': 'Manual Input', + 'rules.manualInputHint': 'Write content directly', + 'rules.cliGenerate': 'CLI Generate', + 'rules.cliGenerateHint': 'Auto-generate via AI', + 'rules.generationType': 'Generation Type', + 'rules.fromDescription': 'From Description', + 'rules.fromTemplate': 'From Template', + 'rules.fromCodeExtract': 'Extract from Code', + 'rules.description': 'Description', + 'rules.descriptionPlaceholder': 'Describe the rule purpose and instructions...', + 'rules.descriptionHint': 'AI will generate rule content based on your description', + 'rules.descriptionRequired': 'Description is required', + 'rules.extractScope': 'Analysis Scope', + 'rules.extractScopeHint': 'File patterns to analyze (e.g., src/**/*.ts)', + 'rules.extractScopeRequired': 'Analysis scope is required', + 'rules.extractFocus': 'Focus Areas', + 'rules.extractFocusHint': 'Comma-separated aspects to focus on (e.g., naming, error-handling)', + 'rules.cliGenerating': 'Generating rule via CLI (this may take a few minutes)...', + + // CLAUDE.md Manager + 'nav.claudeManager': 'CLAUDE.md', + 'title.claudeManager': 'CLAUDE.md Manager', + 'claudeManager.title': 'CLAUDE.md Files', + 'claudeManager.files': 'files', + 'claudeManager.userLevel': 'User Level', + 'claudeManager.projectLevel': 'Project Level', + 'claudeManager.moduleLevel': 'Module Level', + 'claudeManager.noFile': 'No CLAUDE.md file', + 'claudeManager.noModules': 'No module CLAUDE.md files', + 'claudeManager.selectFile': 'Select a file to view', + 'claudeManager.noMetadata': 'Select a file to see metadata', + 'claudeManager.fileInfo': 'File Information', + 'claudeManager.level': 'Level', + 'claudeManager.level_user': 'User (~/.claude/)', + 'claudeManager.level_project': 'Project (.claude/)', + 'claudeManager.level_module': 'Module', + 'claudeManager.path': 'Path', + 'claudeManager.size': 'Size', + 'claudeManager.modified': 'Modified', + 'claudeManager.statistics': 'Statistics', + 'claudeManager.lines': 'Lines', + 'claudeManager.words': 'Words', + 'claudeManager.characters': 'Characters', + 'claudeManager.actions': 'Actions', + 'claudeManager.loadError': 'Failed to load CLAUDE.md files', + 'claudeManager.refreshed': 'Files refreshed successfully', + 'claudeManager.unsavedChanges': 'You have unsaved changes. Discard them?', + 'claudeManager.saved': 'File saved successfully', + 'claudeManager.saveError': 'Failed to save file', + + // CLI Sync (used in claude-manager.js) + 'claude.cliSync': 'CLI Auto-Sync', + 'claude.tool': 'Tool', + 'claude.mode': 'Mode', + 'claude.syncButton': 'Sync with CLI', + 'claude.syncing': 'Analyzing with {tool}...', + 'claude.syncSuccess': '{file} synced successfully', + 'claude.syncError': 'Sync failed: {error}', + 'claude.modeUpdate': 'Update (Smart Merge)', + 'claude.modeGenerate': 'Generate (Full Replace)', + 'claude.modeAppend': 'Append', + 'claude.searchPlaceholder': 'Search files...', + 'claude.viewModeClaude': 'CLAUDE.md Only', + 'claude.viewModeAll': 'All Files', + 'claude.createFile': 'Create File', + 'claude.createDialogTitle': 'Create CLAUDE.md File', + 'claude.selectLevel': 'Level', + 'claude.levelUser': 'User (~/.claude/)', + 'claude.levelProject': 'Project (.claude/)', + 'claude.levelModule': 'Module (custom path)', + 'claude.modulePath': 'Module Path', + 'claude.selectTemplate': 'Template', + 'claude.templateDefault': 'Default', + 'claude.templateMinimal': 'Minimal', + 'claude.templateComprehensive': 'Comprehensive', + 'claude.deleteFile': 'Delete File', + 'claude.deleteConfirm': 'Are you sure you want to delete {file}?', + 'claude.deleteWarning': 'This action cannot be undone.', + 'claude.copyContent': 'Copy Content', + 'claude.contentCopied': 'Content copied to clipboard', + 'claude.copyError': 'Failed to copy content', + 'claude.modulePathRequired': 'Module path is required', + 'claude.fileCreated': 'File created successfully', + 'claude.createFileError': 'Failed to create file', + 'claude.fileDeleted': 'File deleted successfully', + 'claude.deleteFileError': 'Failed to delete file', + 'claude.loadAllFilesError': 'Failed to load all files', + 'claude.unsupportedFileType': 'Unsupported file type', + 'claude.loadFileError': 'Failed to load file', // Common 'common.cancel': 'Cancel', + 'common.optional': '(Optional)', 'common.create': 'Create', 'common.save': 'Save', 'common.delete': 'Delete', @@ -696,6 +920,10 @@ const i18n = { 'common.remove': 'Remove', 'common.removeFromRecent': 'Remove from recent', 'common.noDescription': 'No description', + 'common.saving': 'Saving...', + 'common.saveFailed': 'Failed to save', + 'common.unknownError': 'Unknown error', + 'common.exception': 'Exception', }, zh: { @@ -797,6 +1025,8 @@ const i18n = { 'empty.noLiteSessionsText': '在 .workflow/.{type}/ 目录中未找到会话', 'empty.noMcpServers': '该项目未配置 MCP 服务器', 'empty.addMcpServersHint': '从下方可用列表中添加服务器', + 'empty.noGlobalMcpServers': '未配置全局 MCP 服务器', + 'empty.globalServersHint': '全局服务器对所有项目可用,来自 ~/.claude.json', 'empty.noAdditionalMcp': '其他项目中未找到其他 MCP 服务器', 'empty.noHooks': '该项目未配置钩子', 'empty.createHookHint': '创建钩子以自动化工具使用时的操作', @@ -887,6 +1117,7 @@ const i18n = { 'cli.setDefault': '设为默认', 'cli.default': '默认', 'cli.install': '安装', + 'cli.uninstall': '卸载', 'cli.initIndex': '初始化索引', 'cli.geminiDesc': 'Google AI 代码分析', 'cli.qwenDesc': '阿里通义 AI 助手', @@ -895,6 +1126,80 @@ const i18n = { 'cli.codexLensDescFull': '全文代码搜索引擎', 'cli.semanticDesc': 'AI 驱动的代码理解', 'cli.semanticDescFull': '自然语言代码搜索', + + // CodexLens 配置 + 'codexlens.config': 'CodexLens 配置', + 'codexlens.status': '状态', + 'codexlens.installed': '已安装', + 'codexlens.notInstalled': '未安装', + 'codexlens.indexes': '索引', + 'codexlens.currentWorkspace': '当前工作区', + 'codexlens.indexStoragePath': '索引存储路径', + 'codexlens.whereIndexesStored': '索引存储位置', + 'codexlens.currentPath': '当前路径', + 'codexlens.newStoragePath': '新存储路径', + 'codexlens.pathPlaceholder': '例如:/path/to/indexes 或 ~/.codexlens/indexes', + 'codexlens.pathInfo': '支持 ~ 表示用户目录。更改立即生效。', + 'codexlens.migrationRequired': '需要迁移', + 'codexlens.migrationWarning': '更改路径后,需要为每个工作区重新初始化索引。', + 'codexlens.actions': '操作', + 'codexlens.initializeIndex': '初始化索引', + 'codexlens.cleanAllIndexes': '清理所有索引', + 'codexlens.installCodexLens': '安装 CodexLens', + 'codexlens.testSearch': '测试搜索', + 'codexlens.testFunctionality': '测试 CodexLens 功能', + 'codexlens.textSearch': '文本搜索', + 'codexlens.fileSearch': '文件搜索', + 'codexlens.symbolSearch': '符号搜索', + 'codexlens.searchPlaceholder': '输入搜索查询(例如:函数名、文件路径、代码片段)', + 'codexlens.runSearch': '运行搜索', + 'codexlens.results': '结果', + 'codexlens.resultsCount': '个结果', + 'codexlens.saveConfig': '保存配置', + 'codexlens.searching': '搜索中...', + 'codexlens.searchCompleted': '搜索完成', + 'codexlens.searchFailed': '搜索失败', + 'codexlens.enterQuery': '请输入搜索查询', + 'codexlens.configSaved': '配置保存成功', + 'codexlens.pathEmpty': '索引目录路径不能为空', + 'codexlens.cleanConfirm': '确定要清理所有 CodexLens 索引吗?此操作无法撤销。', + 'codexlens.cleaning': '清理索引中...', + 'codexlens.cleanSuccess': '所有索引已成功清理', + 'codexlens.cleanFailed': '清理索引失败', + 'codexlens.loadingConfig': '加载配置中...', + + // Semantic Search 配置 + 'semantic.settings': '语义搜索设置', + 'semantic.configDesc': '配置语义索引的 LLM 增强功能', + 'semantic.llmEnhancement': 'LLM 增强', + 'semantic.llmDesc': '使用 LLM 生成代码摘要以改进语义搜索', + 'semantic.primaryTool': '主 LLM 工具', + 'semantic.fallbackTool': '备用工具', + 'semantic.batchSize': '批处理大小', + 'semantic.timeout': '超时时间', + 'semantic.file': '个文件', + 'semantic.files': '个文件', + 'semantic.enhanceInfo': 'LLM 增强为每个文件生成代码摘要和关键词,提高语义搜索准确度。', + 'semantic.enhanceCommand': '运行', + 'semantic.enhanceAfterEnable': '启用后处理现有文件。', + 'semantic.runEnhanceNow': '立即运行增强', + 'semantic.viewStatus': '查看状态', + 'semantic.testSearch': '测试语义搜索', + 'semantic.searchPlaceholder': '输入语义查询(例如:身份验证逻辑、错误处理)', + 'semantic.runSearch': '运行语义搜索', + 'semantic.close': '关闭', + 'semantic.enabled': '已启用', + 'semantic.disabled': '已禁用', + 'semantic.toolSetTo': '主 LLM 工具已设置为', + 'semantic.fallbackSetTo': '备用工具已设置为', + 'semantic.none': '无', + 'semantic.llmEnhancement': 'LLM 增强', + 'semantic.batchSetTo': '批量大小已设置为', + 'semantic.timeoutSetTo': '超时已设置为', + 'semantic.minute': '分钟', + 'semantic.minutes': '分钟', + 'semantic.enableFirst': '请先启用 LLM 增强', + 'cli.settings': 'CLI 调用设置', 'cli.promptFormat': '提示词格式', 'cli.promptFormatDesc': '多轮对话拼接格式', @@ -986,9 +1291,18 @@ const i18n = { 'updateClaudeMd.cancel': '取消', // MCP Manager - 'mcp.currentProject': '当前项目 MCP 服务器', + 'mcp.currentAvailable': '当前可用 MCP', + 'mcp.copyInstallCmd': '复制安装命令', + 'mcp.installCmdCopied': '安装命令已复制到剪贴板', + 'mcp.installCmdFailed': '复制安装命令失败', + 'mcp.projectAvailable': '当前可用 MCP', 'mcp.newServer': '新建服务器', + 'mcp.newGlobalServer': '新建全局服务器', + 'mcp.newProjectServer': '新建项目服务器', 'mcp.serversConfigured': '个服务器已配置', + 'mcp.serversAvailable': 'servers available', + 'mcp.globalAvailable': '全局可用 MCP', + 'mcp.globalServersFrom': '个服务器来自 ~/.claude.json', 'mcp.enterprise': '企业 MCP 服务器', 'mcp.enterpriseManaged': '托管', 'mcp.enterpriseReadOnly': '个服务器(只读)', @@ -1005,6 +1319,10 @@ const i18n = { 'mcp.noMcpServers': '无 MCP 服务器', 'mcp.add': '添加', 'mcp.addToProject': '添加到项目', + 'mcp.installToProject': '安装到项目', + 'mcp.installToGlobal': '安装到全局', + 'mcp.installToProject': 'Install to project', + 'mcp.installToGlobal': 'Install to global', 'mcp.removeFromProject': '从项目移除', 'mcp.removeConfirm': '从此项目移除 MCP 服务器 "{name}"?', 'mcp.readOnly': '只读', @@ -1343,6 +1661,30 @@ const i18n = { 'skills.deleteError': '删除技能失败', 'skills.editNotImplemented': '编辑功能即将推出', 'skills.createNotImplemented': '创建功能即将推出', + 'skills.createSkill': '创建技能', 'skills.sourceFolder': '源文件夹', 'skills.sourceFolderPlaceholder': '技能文件夹路径', 'skills.sourceFolderHint': '选择包含有效 SKILL.md 文件的文件夹', 'skills.sourceFolderRequired': '源文件夹路径是必需的', 'skills.customName': '自定义名称', 'skills.customNamePlaceholder': '留空则使用 SKILL.md 中的技能名称', 'skills.customNameHint': '可选:覆盖技能名称', 'skills.validate': '验证', 'skills.import': '导入', 'skills.validating': '验证中...', 'skills.validSkill': '有效技能', 'skills.invalidSkill': '无效技能', 'skills.validateFirst': '请先验证技能文件夹', 'skills.created': '技能 "{name}" 创建成功', 'skills.createError': '创建技能失败', 'skills.validationError': '验证失败', 'skills.enterFolderPath': '输入技能文件夹路径', 'skills.name': '名称', + 'skills.createMode': '创建模式', + 'skills.importFolder': '导入文件夹', + 'skills.importFolderHint': '导入现有技能文件夹', + 'skills.cliGenerate': 'CLI生成', + 'skills.cliGenerateHint': '使用AI生成', + 'skills.generationType': '生成类型', + 'skills.fromDescription': '从描述生成', + 'skills.fromDescriptionHint': '描述你需要的功能', + 'skills.fromTemplate': '从模板生成', + 'skills.comingSoon': '即将推出', + 'skills.skillName': '技能名称', + 'skills.skillNamePlaceholder': '例如:代码审查、测试助手', + 'skills.skillNameHint': '为技能选择一个描述性的名称', + 'skills.skillNameRequired': '技能名称是必需的', + 'skills.descriptionPlaceholder': '描述这个技能应该帮助什么...\n例如:帮助审查代码的安全漏洞和最佳实践', + 'skills.descriptionRequired': '描述是必需的', + 'skills.descriptionGenerateHint': '具体说明技能应该做什么', + 'skills.generating': '正在通过 CLI 生成技能...这可能需要几分钟。', + 'skills.generated': '技能 "{name}" 生成成功', + 'skills.generateError': '生成技能失败', + 'skills.generate': '生成', + 'skills.cliGenerateInfo': 'AI 将根据你的描述生成完整的技能', + 'skills.cliGenerateTimeHint': '生成时间取决于复杂度,可能需要几分钟', // Rules 'nav.rules': '规则', @@ -1369,9 +1711,147 @@ const i18n = { 'rules.deleteError': '删除规则失败', 'rules.editNotImplemented': '编辑功能即将推出', 'rules.createNotImplemented': '创建功能即将推出', + 'rules.createRule': '创建规则', + 'rules.location': '位置', + 'rules.fileName': '文件名', + 'rules.fileNameHint': '必须以 .md 结尾', + 'rules.fileNameRequired': '文件名是必需的', + 'rules.fileNameMustEndMd': '文件名必须以 .md 结尾', + 'rules.subdirectory': '子目录', + 'rules.subdirectoryHint': '可选:将规则组织到子目录中', + 'rules.conditionalRule': '条件规则', + 'rules.conditionalHint': '仅对特定文件路径应用此规则', + 'rules.addPath': '添加路径', + 'rules.contentPlaceholder': '以 Markdown 格式输入规则内容...', + 'rules.contentHint': '使用 Markdown 为 Claude 编写规则说明', + 'rules.contentRequired': '内容是必需的', + 'rules.created': '规则 "{name}" 创建成功', + 'rules.createError': '创建规则失败', + 'rules.createMode': '创建模式', + 'rules.manualInput': '手动输入', + 'rules.manualInputHint': '直接编写内容', + 'rules.cliGenerate': 'CLI生成', + 'rules.cliGenerateHint': '通过AI自动生成', + 'rules.generationType': '生成类型', + 'rules.fromDescription': '从描述生成', + 'rules.fromTemplate': '从模板生成', + 'rules.fromCodeExtract': '从代码提取', + 'rules.description': '描述', + 'rules.descriptionPlaceholder': '描述规则目的和说明...', + 'rules.descriptionHint': 'AI将根据您的描述生成规则内容', + 'rules.descriptionRequired': '描述是必需的', + 'rules.extractScope': '分析范围', + 'rules.extractScopeHint': '要分析的文件模式(例如:src/**/*.ts)', + 'rules.extractScopeRequired': '分析范围是必需的', + 'rules.extractFocus': '关注领域', + 'rules.extractFocusHint': '以逗号分隔的关注方面(例如:命名规范, 错误处理)', + 'rules.cliGenerating': '正在通过 CLI 生成规则(可能需要几分钟)...', + + // CLAUDE.md Manager + 'nav.claudeManager': 'CLAUDE.md', + 'title.claudeManager': 'CLAUDE.md 管理器', + 'claudeManager.title': 'CLAUDE.md 文件', + 'claudeManager.files': '个文件', + 'claudeManager.userLevel': '用户级', + 'claudeManager.projectLevel': '项目级', + 'claudeManager.moduleLevel': '模块级', + 'claudeManager.noFile': '无 CLAUDE.md 文件', + 'claudeManager.noModules': '无模块 CLAUDE.md 文件', + 'claudeManager.selectFile': '选择文件以查看', + 'claudeManager.noMetadata': '选择文件以查看元数据', + 'claudeManager.fileInfo': '文件信息', + 'claudeManager.level': '级别', + 'claudeManager.level_user': '用户级 (~/.claude/)', + 'claudeManager.level_project': '项目级 (.claude/)', + 'claudeManager.level_module': '模块级', + 'claudeManager.path': '路径', + 'claudeManager.size': '大小', + 'claudeManager.modified': '修改时间', + 'claudeManager.statistics': '统计信息', + 'claudeManager.lines': '行数', + 'claudeManager.words': '字数', + 'claudeManager.characters': '字符数', + 'claudeManager.actions': '操作', + 'claudeManager.loadError': '加载 CLAUDE.md 文件失败', + 'claudeManager.refreshed': '文件刷新成功', + 'claudeManager.unsavedChanges': '您有未保存的更改。是否放弃?', + 'claudeManager.saved': '文件保存成功', + 'claudeManager.saveError': '文件保存失败', + + // CLI Sync (used in claude-manager.js) + 'claude.cliSync': 'CLI 自动同步', + 'claude.tool': '工具', + 'claude.mode': '模式', + 'claude.syncButton': '使用 CLI 同步', + 'claude.syncing': '正在使用 {tool} 分析...', + 'claude.syncSuccess': '{file} 同步成功', + 'claude.syncError': '同步失败:{error}', + 'claude.modeUpdate': '更新(智能合并)', + 'claude.modeGenerate': '生成(完全替换)', + 'claude.modeAppend': '追加', + 'claude.searchPlaceholder': '搜索文件...', + 'claude.viewModeClaude': '仅 CLAUDE.md', + 'claude.viewModeAll': '所有文件', + 'claude.createFile': '创建文件', + 'claude.createDialogTitle': '创建 CLAUDE.md 文件', + 'claude.selectLevel': '层级', + 'claude.levelUser': '用户级 (~/.claude/)', + 'claude.levelProject': '项目级 (.claude/)', + 'claude.levelModule': '模块级(自定义路径)', + 'claude.modulePath': '模块路径', + 'claude.selectTemplate': '模板', + 'claude.templateDefault': '默认', + 'claude.templateMinimal': '最小化', + 'claude.templateComprehensive': '完整', + 'claude.deleteFile': '删除文件', + 'claude.deleteConfirm': '确定要删除 {file} 吗?', + 'claude.deleteWarning': '此操作无法撤销。', + 'claude.copyContent': '复制内容', + 'claude.contentCopied': '内容已复制到剪贴板', + 'claude.copyError': '复制内容失败', + 'claude.modulePathRequired': '模块路径为必填项', + 'claude.fileCreated': '文件创建成功', + 'claude.createFileError': '文件创建失败', + 'claude.fileDeleted': '文件删除成功', + 'claude.deleteFileError': '文件删除失败', + 'claude.loadAllFilesError': '加载所有文件失败', + 'claude.unsupportedFileType': '不支持的文件类型', + 'claude.loadFileError': '加载文件失败', + + // Duplicate keys for compatibility + 'nav.claudeManager': 'CLAUDE.md', + 'title.claudeManager': 'CLAUDE.md Manager', + 'claudeManager.title': 'CLAUDE.md Files', + 'claudeManager.files': 'files', + 'claudeManager.userLevel': 'User Level', + 'claudeManager.projectLevel': 'Project Level', + 'claudeManager.moduleLevel': 'Module Level', + 'claudeManager.noFile': 'No CLAUDE.md file', + 'claudeManager.noModules': 'No module CLAUDE.md files', + 'claudeManager.selectFile': 'Select a file to view', + 'claudeManager.noMetadata': 'Select a file to see metadata', + 'claudeManager.fileInfo': 'File Information', + 'claudeManager.level': 'Level', + 'claudeManager.level_user': 'User (~/.claude/)', + 'claudeManager.level_project': 'Project (.claude/)', + 'claudeManager.level_module': 'Module', + 'claudeManager.path': 'Path', + 'claudeManager.size': 'Size', + 'claudeManager.modified': 'Modified', + 'claudeManager.statistics': 'Statistics', + 'claudeManager.lines': 'Lines', + 'claudeManager.words': 'Words', + 'claudeManager.characters': 'Characters', + 'claudeManager.actions': 'Actions', + 'claudeManager.loadError': 'Failed to load CLAUDE.md files', + 'claudeManager.refreshed': 'Files refreshed successfully', + 'claudeManager.unsavedChanges': 'You have unsaved changes. Discard them?', + 'claudeManager.saved': 'File saved successfully', + 'claudeManager.saveError': 'Failed to save file', // Common 'common.cancel': '取消', + 'common.optional': '(可选)', 'common.create': '创建', 'common.save': '保存', 'common.delete': '删除', @@ -1386,6 +1866,10 @@ const i18n = { 'common.remove': '移除', 'common.removeFromRecent': '从最近中移除', 'common.noDescription': '无描述', + 'common.saving': '保存中...', + 'common.saveFailed': '保存失败', + 'common.unknownError': '未知错误', + 'common.exception': '异常', } }; diff --git a/ccw/src/templates/dashboard-js/views/claude-manager.js b/ccw/src/templates/dashboard-js/views/claude-manager.js new file mode 100644 index 00000000..b19e979b --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/claude-manager.js @@ -0,0 +1,764 @@ +// CLAUDE.md Manager View +// Three-column layout: File Tree | Viewer/Editor | Metadata & Actions + +// ========== State Management ========== +var claudeFilesData = { + user: { main: null }, + project: { main: null }, + modules: [], + summary: { totalFiles: 0, totalSize: 0 } +}; +var selectedFile = null; +var isEditMode = false; +var isDirty = false; +var fileTreeExpanded = { + user: true, + project: true, + modules: {} +}; +var searchQuery = ''; + +// ========== Main Render Function ========== +async function renderClaudeManager() { + var container = document.getElementById('mainContent'); + if (!container) return; + + // Hide stats grid and search for claude-manager view + var statsGrid = document.getElementById('statsGrid'); + var searchInput = document.getElementById('searchInput'); + if (statsGrid) statsGrid.style.display = 'none'; + if (searchInput) searchInput.parentElement.style.display = 'none'; + + // Show loading state + container.innerHTML = '
' + + '
' + + '

' + t('common.loading') + '

' + + '
'; + + // Load data + await loadClaudeFiles(); + + // Render layout + container.innerHTML = '
' + + '
' + + '
' + + '

' + t('claudeManager.title') + '

' + + '' + claudeFilesData.summary.totalFiles + ' ' + t('claudeManager.files') + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + + // Render each column + renderFileTree(); + renderFileViewer(); + renderFileMetadata(); + + // Initialize Lucide icons + if (window.lucide) lucide.createIcons(); +} + +// ========== Data Loading ========== +async function loadClaudeFiles() { + try { + var res = await fetch('/api/memory/claude/scan?path=' + encodeURIComponent(projectPath || '')); + if (!res.ok) throw new Error('Failed to load CLAUDE.md files'); + claudeFilesData = await res.json(); + updateClaudeBadge(); // Update navigation badge + } catch (error) { + console.error('Error loading CLAUDE.md files:', error); + addGlobalNotification('error', t('claudeManager.loadError'), null, 'CLAUDE.md'); + } +} + +async function refreshClaudeFiles() { + await loadClaudeFiles(); + await renderClaudeManager(); + addGlobalNotification('success', t('claudeManager.refreshed'), null, 'CLAUDE.md'); +} + +// ========== File Tree Rendering ========== +function renderFileTree() { + var container = document.getElementById('claude-file-tree'); + if (!container) return; + + var html = '
' + + // Search Box + '' + + renderClaudeFilesTree() + + '
'; // end file-tree + + container.innerHTML = html; + if (window.lucide) lucide.createIcons(); +} + +function renderClaudeFilesTree() { + var html = '
' + + '
' + + '' + + '' + + '' + t('claudeManager.userLevel') + '' + + '' + (claudeFilesData.user.main ? 1 : 0) + '' + + '
'; + + if (fileTreeExpanded.user) { + // User CLAUDE.md (only main file, no rules) + if (claudeFilesData.user.main) { + html += renderFileTreeItem(claudeFilesData.user.main, 1); + } else { + html += '
' + + '' + + '' + t('claudeManager.noFile') + '' + + '
'; + } + } + + html += '
'; // end user section + + // Project section + html += '
' + + '
' + + '' + + '' + + '' + t('claudeManager.projectLevel') + '' + + '' + (claudeFilesData.project.main ? 1 : 0) + '' + + '
'; + + if (fileTreeExpanded.project) { + // Project CLAUDE.md (only main file, no rules) + if (claudeFilesData.project.main) { + html += renderFileTreeItem(claudeFilesData.project.main, 1); + } else { + html += '
' + + '' + + '' + t('claudeManager.noFile') + '' + + '
'; + } + } + + html += '
'; // end project section + + // Modules section + html += '
' + + '
' + + '' + + '' + t('claudeManager.moduleLevel') + '' + + '' + claudeFilesData.modules.length + '' + + '
'; + + if (claudeFilesData.modules.length > 0) { + claudeFilesData.modules.forEach(function (file) { + html += renderFileTreeItem(file, 1); + }); + } else { + html += '
' + + '' + + '' + t('claudeManager.noModules') + '' + + '
'; + } + + html += '
'; // end modules section + + return html; +} + +function renderFileTreeItem(file, indentLevel) { + var isSelected = selectedFile && selectedFile.id === file.id; + var indentPx = indentLevel * 1.5; + var safeId = file.id.replace(/'/g, "'"); + + return '
' + + '' + + '' + escapeHtml(file.name) + '' + + (file.parentDirectory ? '' + escapeHtml(file.parentDirectory) + '' : '') + + '
'; +} + +function toggleTreeSection(section) { + fileTreeExpanded[section] = !fileTreeExpanded[section]; + renderFileTree(); +} + +async function selectClaudeFile(fileId) { + // Find file in data (only main CLAUDE.md files, no rules) + var allFiles = [ + claudeFilesData.user.main, + claudeFilesData.project.main, + ...claudeFilesData.modules + ].filter(function (f) { return f !== null; }); + + selectedFile = allFiles.find(function (f) { return f.id === fileId; }) || null; + + if (selectedFile) { + // Load full content if not already loaded + if (!selectedFile.content) { + try { + var res = await fetch('/api/memory/claude/file?path=' + encodeURIComponent(selectedFile.path)); + if (res.ok) { + var data = await res.json(); + selectedFile.content = data.content; + selectedFile.stats = data.stats; + } + } catch (error) { + console.error('Error loading file content:', error); + } + } + } + + renderFileTree(); + renderFileViewer(); + renderFileMetadata(); +} + +// ========== File Viewer Rendering ========== +function renderFileViewer() { + var container = document.getElementById('claude-file-viewer'); + if (!container) return; + + if (!selectedFile) { + container.innerHTML = '
' + + '' + + '

' + t('claudeManager.selectFile') + '

' + + '
'; + if (window.lucide) lucide.createIcons(); + return; + } + + container.innerHTML = '
' + + '
' + + '

' + escapeHtml(selectedFile.name) + '

' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + (isEditMode ? renderEditor() : renderMarkdownContent(selectedFile.content || '')) + + '
' + + '
'; + + if (window.lucide) lucide.createIcons(); +} + +function renderMarkdownContent(content) { + // Check if marked.js is available for enhanced rendering + if (typeof marked !== 'undefined') { + try { + marked.setOptions({ + gfm: true, + breaks: true, + tables: true, + smartLists: true, + highlight: function(code, lang) { + // Check if highlight.js or Prism is available + if (typeof hljs !== 'undefined' && lang) { + try { + return hljs.highlight(code, { language: lang }).value; + } catch (e) { + return escapeHtml(code); + } + } else if (typeof Prism !== 'undefined' && lang && Prism.languages[lang]) { + return Prism.highlight(code, Prism.languages[lang], lang); + } + return escapeHtml(code); + } + }); + return '
' + marked.parse(content) + '
'; + } catch (e) { + console.error('Error rendering markdown with marked.js:', e); + } + } + + // Fallback: Enhanced basic rendering + var html = escapeHtml(content); + + // Headers + html = html + .replace(/^# (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^### (.*$)/gim, '

$1

') + .replace(/^#### (.*$)/gim, '

$1

'); + + // Inline formatting + html = html + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/`([^`]+)`/g, '$1'); + + // Links + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Task lists + html = html + .replace(/- \[ \] (.+)$/gim, '
  • $1
  • ') + .replace(/- \[x\] (.+)$/gim, '
  • $1
  • '); + + // Lists + html = html.replace(/^- (.+)$/gim, '
  • $1
  • '); + html = html.replace(/(
  • .*<\/li>)/s, '
      $1
    '); + + // Code blocks + html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, function(match, lang, code) { + return '
    ' + code + '
    '; + }); + + // Line breaks + html = html.replace(/\n/g, '
    '); + + return '
    ' + html + '
    '; +} + +function renderEditor() { + return ''; +} + +function toggleEditMode() { + if (isEditMode && isDirty) { + if (!confirm(t('claudeManager.unsavedChanges'))) { + return; + } + } + + isEditMode = !isEditMode; + isDirty = false; + renderFileViewer(); +} + +function markDirty() { + isDirty = true; +} + +async function saveClaudeFile() { + if (!selectedFile || !isEditMode) return; + + var editor = document.getElementById('claudeFileEditor'); + if (!editor) return; + + var newContent = editor.value; + + try { + var res = await fetch('/api/memory/claude/file', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + path: selectedFile.path, + content: newContent, + createBackup: true + }) + }); + + if (!res.ok) throw new Error('Failed to save file'); + + selectedFile.content = newContent; + selectedFile.stats = calculateFileStats(newContent); + isDirty = false; + + addGlobalNotification('success', t('claudeManager.saved'), null, 'CLAUDE.md'); + renderFileMetadata(); + } catch (error) { + console.error('Error saving file:', error); + addGlobalNotification('error', t('claudeManager.saveError'), null, 'CLAUDE.md'); + } +} + +function calculateFileStats(content) { + var lines = content.split('\n').length; + var words = content.split(/\s+/).filter(function (w) { return w.length > 0; }).length; + var characters = content.length; + return { lines: lines, words: words, characters: characters }; +} + +// ========== File Metadata Rendering ========== +function renderFileMetadata() { + var container = document.getElementById('claude-file-metadata'); + if (!container) return; + + if (!selectedFile) { + container.innerHTML = '
    ' + + '' + + '

    ' + t('claudeManager.noMetadata') + '

    ' + + '
    '; + if (window.lucide) lucide.createIcons(); + return; + } + + var html = ''; // end file-metadata + + container.innerHTML = html; + if (window.lucide) lucide.createIcons(); +} + +// ========== CLI Sync Functions ========== +async function syncFileWithCLI() { + if (!selectedFile) return; + + var tool = document.getElementById('cliToolSelect').value; + var mode = document.getElementById('cliModeSelect').value; + + // Show progress + showSyncProgress(true, tool); + + // Disable sync button + var syncButton = document.getElementById('cliSyncButton'); + if (syncButton) syncButton.disabled = true; + + try { + var response = await fetch('/api/memory/claude/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + level: selectedFile.level, + path: selectedFile.level === 'module' ? selectedFile.path.replace(/CLAUDE\.md$/, '').replace(/\/$/, '') : undefined, + tool: tool, + mode: mode + }) + }); + + var result = await response.json(); + + if (result.success) { + // Reload file content + var fileData = await loadFileContent(selectedFile.path); + if (fileData) { + selectedFile = fileData; + renderFileViewer(); + renderFileMetadata(); + } + showClaudeNotification('success', (t('claude.syncSuccess') || 'Synced successfully').replace('{file}', selectedFile.name)); + } else { + showClaudeNotification('error', (t('claude.syncError') || 'Sync failed').replace('{error}', result.error || 'Unknown error')); + } + } catch (error) { + console.error('CLI sync error:', error); + showClaudeNotification('error', (t('claude.syncError') || 'Sync failed').replace('{error}', error.message)); + } finally { + showSyncProgress(false); + if (syncButton) syncButton.disabled = false; + } +} + +function showSyncProgress(show, tool) { + var progressEl = document.getElementById('syncProgress'); + var progressText = document.getElementById('syncProgressText'); + if (!progressEl) return; + + if (show) { + progressEl.style.display = 'flex'; + if (progressText) { + var text = (t('claude.syncing') || 'Analyzing with {tool}...').replace('{tool}', tool || 'CLI'); + progressText.textContent = text; + } + if (window.lucide) lucide.createIcons(); + } else { + progressEl.style.display = 'none'; + } +} + +async function loadFileContent(filePath) { + try { + var res = await fetch('/api/memory/claude/file?path=' + encodeURIComponent(filePath)); + if (!res.ok) return null; + return await res.json(); + } catch (error) { + console.error('Error loading file content:', error); + return null; + } +} + +function showClaudeNotification(type, message) { + // Use global notification system if available + if (typeof addGlobalNotification === 'function') { + addGlobalNotification(type, message, null, 'CLAUDE.md'); + } else { + // Fallback to simple alert + alert(message); + } +} + +// ========== Search Functions ========== +function filterFileTree(query) { + searchQuery = query.toLowerCase(); + renderFileTree(); + + // Add keyboard shortcut handler + if (query && !window.claudeSearchKeyboardHandlerAdded) { + document.addEventListener('keydown', handleSearchKeyboard); + window.claudeSearchKeyboardHandlerAdded = true; + } +} + +function handleSearchKeyboard(e) { + // Ctrl+F or Cmd+F + if ((e.ctrlKey || e.metaKey) && e.key === 'f') { + e.preventDefault(); + var searchInput = document.getElementById('fileSearchInput'); + if (searchInput) { + searchInput.focus(); + searchInput.select(); + } + } +} + +// ========== File Creation Functions ========== +function showCreateFileDialog() { + var dialog = ''; + + document.body.insertAdjacentHTML('beforeend', dialog); + if (window.lucide) lucide.createIcons(); +} + +function closeCreateDialog() { + var overlay = document.querySelector('.modal-overlay'); + if (overlay) overlay.remove(); +} + +function toggleModulePathInput(level) { + var pathLabel = document.getElementById('modulePathLabel'); + var pathInput = document.getElementById('modulePath'); + + if (level === 'module') { + pathLabel.style.display = 'block'; + pathInput.style.display = 'block'; + } else { + pathLabel.style.display = 'none'; + pathInput.style.display = 'none'; + } +} + +async function createNewFile() { + var level = document.getElementById('createLevel').value; + var template = document.getElementById('createTemplate').value; + var modulePath = document.getElementById('modulePath').value; + + if (level === 'module' && !modulePath) { + addGlobalNotification('error', t('claude.modulePathRequired') || 'Module path is required', null, 'CLAUDE.md'); + return; + } + + try { + var res = await fetch('/api/memory/claude/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + level: level, + path: modulePath || undefined, + template: template + }) + }); + + if (!res.ok) throw new Error('Failed to create file'); + + var result = await res.json(); + closeCreateDialog(); + addGlobalNotification('success', t('claude.fileCreated') || 'File created successfully', null, 'CLAUDE.md'); + + // Refresh file tree + await refreshClaudeFiles(); + } catch (error) { + console.error('Error creating file:', error); + addGlobalNotification('error', t('claude.createFileError') || 'Failed to create file', null, 'CLAUDE.md'); + } +} + +// ========== File Deletion Functions ========== +async function confirmDeleteFile() { + if (!selectedFile) return; + + var confirmed = confirm( + (t('claude.deleteConfirm') || 'Are you sure you want to delete {file}?').replace('{file}', selectedFile.name) + '\n\n' + + 'Path: ' + selectedFile.path + '\n\n' + + (t('claude.deleteWarning') || 'This action cannot be undone.') + ); + + if (!confirmed) return; + + try { + var res = await fetch('/api/memory/claude/file?path=' + encodeURIComponent(selectedFile.path) + '&confirm=true', { + method: 'DELETE' + }); + + if (!res.ok) throw new Error('Failed to delete file'); + + addGlobalNotification('success', t('claude.fileDeleted') || 'File deleted successfully', null, 'CLAUDE.md'); + selectedFile = null; + + // Refresh file tree + await refreshClaudeFiles(); + } catch (error) { + console.error('Error deleting file:', error); + addGlobalNotification('error', t('claude.deleteFileError') || 'Failed to delete file', null, 'CLAUDE.md'); + } +} + +// ========== Copy Content Function ========== +function copyFileContent() { + if (!selectedFile || !selectedFile.content) return; + + navigator.clipboard.writeText(selectedFile.content).then(function() { + addGlobalNotification('success', t('claude.contentCopied') || 'Content copied to clipboard', null, 'CLAUDE.md'); + }).catch(function(error) { + console.error('Error copying content:', error); + addGlobalNotification('error', t('claude.copyError') || 'Failed to copy content', null, 'CLAUDE.md'); + }); +} + +// ========== Utility Functions ========== +// Note: escapeHtml and formatDate are imported from utils.js + +function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; +} + +// Update navigation badge with total file count +function updateClaudeBadge() { + var badge = document.getElementById('badgeClaude'); + if (badge && claudeFilesData && claudeFilesData.summary) { + var total = claudeFilesData.summary.totalFiles; + badge.textContent = total; + } +} diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index c6803840..52e49d20 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -207,32 +207,14 @@ function initToolConfigModalEvents(tool, currentConfig, models) { // Install/Uninstall var installBtn = document.getElementById('installBtn'); if (installBtn) { - installBtn.onclick = async function() { + installBtn.onclick = function() { var status = cliToolStatus[tool] || {}; - var endpoint = status.available ? '/api/cli/uninstall' : '/api/cli/install'; - var action = status.available ? 'uninstalling' : 'installing'; - - showRefreshToast(tool.charAt(0).toUpperCase() + tool.slice(1) + ' ' + action + '...', 'info'); closeModal(); - try { - var response = await fetch(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ tool: tool }) - }); - var result = await response.json(); - - if (result.success) { - showRefreshToast(result.message || (tool + ' ' + (status.available ? 'uninstalled' : 'installed')), 'success'); - await loadCliToolStatus(); - renderToolsSection(); - if (window.lucide) lucide.createIcons(); - } else { - showRefreshToast(result.error || 'Operation failed', 'error'); - } - } catch (err) { - showRefreshToast('Failed: ' + err.message, 'error'); + if (status.available) { + openCliUninstallWizard(tool); + } else { + openCliInstallWizard(tool); } }; } @@ -384,20 +366,22 @@ function renderToolsSection() { }).join(''); // CodexLens item - var codexLensHtml = '
    ' + + var codexLensHtml = '
    ' + '
    ' + '' + '
    ' + - '
    CodexLens Index
    ' + + '
    CodexLens Index' + + '
    ' + '
    ' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '
    ' + '
    ' + '
    ' + '
    ' + (codexLensStatus.ready ? ' v' + (codexLensStatus.version || 'installed') + '' + - '' + '' + + '' : ' ' + t('cli.notInstalled') + '' + - '') + + '') + '
    ' + '
    '; @@ -1203,3 +1187,607 @@ function handleCliExecutionError(payload) { currentCliExecution = null; } + +// ========== CLI Tool Install/Uninstall Wizards ========== +function openCliInstallWizard(toolName) { + var toolDescriptions = { + gemini: 'Google AI for code analysis and generation', + qwen: 'Alibaba AI assistant for coding', + codex: 'OpenAI code generation and understanding', + claude: 'Anthropic AI assistant' + }; + + var toolPackages = { + gemini: '@google/gemini-cli', + qwen: '@qwen-code/qwen-code', + codex: '@openai/codex', + claude: '@anthropic-ai/claude-code' + }; + + var modal = document.createElement('div'); + modal.id = 'cliInstallModal'; + modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; + modal.innerHTML = + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '

    Install ' + toolName.charAt(0).toUpperCase() + toolName.slice(1) + '

    ' + + '

    ' + (toolDescriptions[toolName] || 'CLI tool') + '

    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '

    What will be installed:

    ' + + '
      ' + + '
    • ' + + '' + + 'NPM Package: ' + (toolPackages[toolName] || toolName) + '' + + '
    • ' + + '
    • ' + + '' + + 'Global installation - Available system-wide' + + '
    • ' + + '
    • ' + + '' + + 'CLI commands - Accessible from terminal' + + '
    • ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '

    Installation Method

    ' + + '

    Uses npm install -g

    ' + + '

    First installation may take 1-2 minutes depending on network speed.

    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    '; + + document.body.appendChild(modal); + + if (window.lucide) { + lucide.createIcons(); + } +} + +function closeCliInstallWizard() { + var modal = document.getElementById('cliInstallModal'); + if (modal) { + modal.remove(); + } +} + +async function startCliInstall(toolName) { + var progressDiv = document.getElementById('cliInstallProgress'); + var installBtn = document.getElementById('cliInstallBtn'); + var statusText = document.getElementById('cliInstallStatus'); + var progressBar = document.getElementById('cliInstallProgressBar'); + + progressDiv.classList.remove('hidden'); + installBtn.disabled = true; + installBtn.innerHTML = 'Installing...'; + + var stages = [ + { progress: 20, text: 'Connecting to NPM registry...' }, + { progress: 40, text: 'Downloading package...' }, + { progress: 60, text: 'Installing dependencies...' }, + { progress: 80, text: 'Setting up CLI commands...' }, + { progress: 95, text: 'Finalizing installation...' } + ]; + + var currentStage = 0; + var progressInterval = setInterval(function() { + if (currentStage < stages.length) { + statusText.textContent = stages[currentStage].text; + progressBar.style.width = stages[currentStage].progress + '%'; + currentStage++; + } + }, 1000); + + try { + var response = await fetch('/api/cli/install', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tool: toolName }) + }); + + clearInterval(progressInterval); + var result = await response.json(); + + if (result.success) { + progressBar.style.width = '100%'; + statusText.textContent = 'Installation complete!'; + + setTimeout(function() { + closeCliInstallWizard(); + showRefreshToast(toolName + ' installed successfully!', 'success'); + loadCliToolStatus().then(function() { + renderToolsSection(); + if (window.lucide) lucide.createIcons(); + }); + }, 1000); + } else { + statusText.textContent = 'Error: ' + result.error; + progressBar.classList.add('bg-destructive'); + installBtn.disabled = false; + installBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); + } + } catch (err) { + clearInterval(progressInterval); + statusText.textContent = 'Error: ' + err.message; + progressBar.classList.add('bg-destructive'); + installBtn.disabled = false; + installBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); + } +} + +function openCliUninstallWizard(toolName) { + var modal = document.createElement('div'); + modal.id = 'cliUninstallModal'; + modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; + modal.innerHTML = + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '

    Uninstall ' + toolName.charAt(0).toUpperCase() + toolName.slice(1) + '

    ' + + '

    Remove CLI tool from system

    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '

    What will be removed:

    ' + + '
      ' + + '
    • ' + + '' + + 'Global NPM package' + + '
    • ' + + '
    • ' + + '' + + 'CLI commands and executables' + + '
    • ' + + '
    • ' + + '' + + 'Tool configuration (if any)' + + '
    • ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '

    Note

    ' + + '

    You can reinstall this tool anytime from the CLI Manager.

    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    '; + + document.body.appendChild(modal); + + if (window.lucide) { + lucide.createIcons(); + } +} + +function closeCliUninstallWizard() { + var modal = document.getElementById('cliUninstallModal'); + if (modal) { + modal.remove(); + } +} + +async function startCliUninstall(toolName) { + var progressDiv = document.getElementById('cliUninstallProgress'); + var uninstallBtn = document.getElementById('cliUninstallBtn'); + var statusText = document.getElementById('cliUninstallStatus'); + var progressBar = document.getElementById('cliUninstallProgressBar'); + + progressDiv.classList.remove('hidden'); + uninstallBtn.disabled = true; + uninstallBtn.innerHTML = 'Uninstalling...'; + + var stages = [ + { progress: 33, text: 'Removing package files...' }, + { progress: 66, text: 'Cleaning up dependencies...' }, + { progress: 90, text: 'Finalizing removal...' } + ]; + + var currentStage = 0; + var progressInterval = setInterval(function() { + if (currentStage < stages.length) { + statusText.textContent = stages[currentStage].text; + progressBar.style.width = stages[currentStage].progress + '%'; + currentStage++; + } + }, 500); + + try { + var response = await fetch('/api/cli/uninstall', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tool: toolName }) + }); + + clearInterval(progressInterval); + var result = await response.json(); + + if (result.success) { + progressBar.style.width = '100%'; + statusText.textContent = 'Uninstallation complete!'; + + setTimeout(function() { + closeCliUninstallWizard(); + showRefreshToast(toolName + ' uninstalled successfully!', 'success'); + loadCliToolStatus().then(function() { + renderToolsSection(); + if (window.lucide) lucide.createIcons(); + }); + }, 1000); + } else { + statusText.textContent = 'Error: ' + result.error; + progressBar.classList.remove('bg-destructive'); + progressBar.classList.add('bg-destructive'); + uninstallBtn.disabled = false; + uninstallBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); + } + } catch (err) { + clearInterval(progressInterval); + statusText.textContent = 'Error: ' + err.message; + progressBar.classList.remove('bg-destructive'); + progressBar.classList.add('bg-destructive'); + uninstallBtn.disabled = false; + uninstallBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); + } +} + +// ========== CodexLens Configuration Modal ========== +async function showCodexLensConfigModal() { + var loadingContent = '
    ' + + '
    ' + + '

    ' + t('codexlens.loadingConfig') + '

    ' + + '
    '; + + showModal(t('codexlens.config'), loadingContent, { size: 'md' }); + + try { + // Fetch current configuration + var response = await fetch('/api/codexlens/config'); + var config = await response.json(); + + var content = buildCodexLensConfigContent(config); + showModal('CodexLens Configuration', content, { size: 'md' }); + + setTimeout(function() { + initCodexLensConfigEvents(config); + if (window.lucide) lucide.createIcons(); + }, 100); + } catch (err) { + var errorContent = '
    ' + + '
    ' + + '' + + '
    ' + + '

    Failed to load configuration

    ' + + '

    ' + err.message + '

    ' + + '
    ' + + '
    ' + + '
    '; + showModal('CodexLens Configuration', errorContent, { size: 'md' }); + } +} + +function buildCodexLensConfigContent(config) { + var status = codexLensStatus || {}; + var isInstalled = status.ready; + var indexDir = config.index_dir || '~/.codexlens/indexes'; + var currentWorkspace = config.current_workspace || 'None'; + var indexCount = config.index_count || 0; + + return '
    ' + + // Status Section + '
    ' + + '

    ' + t('codexlens.status') + '

    ' + + '
    ' + + '' + + ' ' + + (isInstalled ? t('codexlens.installed') : t('codexlens.notInstalled')) + + '' + + '' + + ' ' + indexCount + ' ' + t('codexlens.indexes') + + '' + + '
    ' + + (currentWorkspace !== 'None' + ? '
    ' + + '

    ' + t('codexlens.currentWorkspace') + ':

    ' + + '

    ' + escapeHtml(currentWorkspace) + '

    ' + + '
    ' + : '') + + '
    ' + + + // Index Storage Path Section + '
    ' + + '

    ' + t('codexlens.indexStoragePath') + ' (' + t('codexlens.whereIndexesStored') + ')

    ' + + '
    ' + + '
    ' + + '

    ' + t('codexlens.currentPath') + ':

    ' + + '

    ' + + escapeHtml(indexDir) + + '

    ' + + '
    ' + + '
    ' + + '' + + '' + + '

    ' + + ' ' + + t('codexlens.pathInfo') + + '

    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '

    ' + t('codexlens.migrationRequired') + '

    ' + + '

    ' + t('codexlens.migrationWarning') + '

    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + + // Actions Section + '
    ' + + '

    ' + t('codexlens.actions') + '

    ' + + '
    ' + + (isInstalled + ? '' + + '' + + '' + : '') + + '
    ' + + '
    ' + + + // Test Search Section + (isInstalled + ? '
    ' + + '

    ' + t('codexlens.testSearch') + ' (' + t('codexlens.testFunctionality') + ')

    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '' + + '
    ' + + '' + + '
    ' + + '
    ' + : '') + + + // Footer + '' + + '
    '; +} + +function initCodexLensConfigEvents(currentConfig) { + var saveBtn = document.getElementById('saveCodexLensConfigBtn'); + if (saveBtn) { + saveBtn.onclick = async function() { + var indexDirInput = document.getElementById('indexDirInput'); + var newIndexDir = indexDirInput ? indexDirInput.value.trim() : ''; + + if (!newIndexDir) { + showRefreshToast(t('codexlens.pathEmpty'), 'error'); + return; + } + + if (newIndexDir === currentConfig.index_dir) { + closeModal(); + return; + } + + saveBtn.disabled = true; + saveBtn.innerHTML = '' + t('common.saving') + ''; + + try { + var response = await fetch('/api/codexlens/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ index_dir: newIndexDir }) + }); + + var result = await response.json(); + + if (result.success) { + showRefreshToast(t('codexlens.configSaved'), 'success'); + closeModal(); + + // Refresh CodexLens status + if (typeof loadCodexLensStatus === 'function') { + await loadCodexLensStatus(); + renderToolsSection(); + if (window.lucide) lucide.createIcons(); + } + } else { + showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); + saveBtn.disabled = false; + saveBtn.innerHTML = ' ' + t('codexlens.saveConfig'); + if (window.lucide) lucide.createIcons(); + } + } catch (err) { + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + saveBtn.disabled = false; + saveBtn.innerHTML = ' ' + t('codexlens.saveConfig'); + if (window.lucide) lucide.createIcons(); + } + }; + } + + // Test Search Button + var runSearchBtn = document.getElementById('runSearchBtn'); + if (runSearchBtn) { + runSearchBtn.onclick = async function() { + var searchType = document.getElementById('searchTypeSelect').value; + var query = document.getElementById('searchQueryInput').value.trim(); + var resultsDiv = document.getElementById('searchResults'); + var resultCount = document.getElementById('searchResultCount'); + var resultContent = document.getElementById('searchResultContent'); + + if (!query) { + showRefreshToast(t('codexlens.enterQuery'), 'warning'); + return; + } + + runSearchBtn.disabled = true; + runSearchBtn.innerHTML = '' + t('codexlens.searching') + ''; + resultsDiv.classList.add('hidden'); + + try { + var endpoint = '/api/codexlens/' + searchType; + var params = new URLSearchParams({ query: query, limit: '20' }); + + var response = await fetch(endpoint + '?' + params.toString()); + var result = await response.json(); + + console.log('[CodexLens Test] Search result:', result); + + if (result.success) { + var results = result.results || result.files || []; + resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount'); + resultContent.textContent = JSON.stringify(results, null, 2); + resultsDiv.classList.remove('hidden'); + showRefreshToast(t('codexlens.searchCompleted') + ': ' + results.length + ' ' + t('codexlens.resultsCount'), 'success'); + } else { + resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError')); + resultsDiv.classList.remove('hidden'); + showRefreshToast(t('codexlens.searchFailed') + ': ' + result.error, 'error'); + } + + runSearchBtn.disabled = false; + runSearchBtn.innerHTML = ' ' + t('codexlens.runSearch'); + if (window.lucide) lucide.createIcons(); + } catch (err) { + console.error('[CodexLens Test] Error:', err); + resultContent.textContent = t('common.exception') + ': ' + err.message; + resultsDiv.classList.remove('hidden'); + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + runSearchBtn.disabled = false; + runSearchBtn.innerHTML = ' ' + t('codexlens.runSearch'); + if (window.lucide) lucide.createIcons(); + } + }; + } +} + +async function cleanCodexLensIndexes() { + if (!confirm(t('codexlens.cleanConfirm'))) { + return; + } + + try { + showRefreshToast(t('codexlens.cleaning'), 'info'); + + var response = await fetch('/api/codexlens/clean', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ all: true }) + }); + + var result = await response.json(); + + if (result.success) { + showRefreshToast(t('codexlens.cleanSuccess'), 'success'); + + // Refresh status + if (typeof loadCodexLensStatus === 'function') { + await loadCodexLensStatus(); + renderToolsSection(); + if (window.lucide) lucide.createIcons(); + } + } else { + showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error'); + } + } catch (err) { + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + } +} diff --git a/ccw/src/templates/dashboard-js/views/mcp-manager.js b/ccw/src/templates/dashboard-js/views/mcp-manager.js index 4cfd7912..afe6fc74 100644 --- a/ccw/src/templates/dashboard-js/views/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/views/mcp-manager.js @@ -47,18 +47,71 @@ async function renderMcpManager() { const projectData = mcpAllProjects[currentPath] || {}; const projectServers = projectData.mcpServers || {}; const disabledServers = projectData.disabledMcpServers || []; + const hasMcpJson = projectData.hasMcpJson || false; + const mcpJsonPath = projectData.mcpJsonPath || null; // Get all available servers from all projects const allAvailableServers = getAllAvailableMcpServers(); - // Separate current project servers and available servers - const currentProjectServerNames = Object.keys(projectServers); + // Separate servers by category: + // 1. Project Available = Global + Project-specific (servers available to current project) + // 2. Global Management = Global servers that can be managed + // 3. Other Projects = Servers from other projects (can install to project or global) - // Separate enterprise, user, and other project servers - const enterpriseServerEntries = Object.entries(mcpEnterpriseServers || {}) - .filter(([name]) => !currentProjectServerNames.includes(name)); - const userServerEntries = Object.entries(mcpUserServers || {}) - .filter(([name]) => !currentProjectServerNames.includes(name) && !(mcpEnterpriseServers || {})[name]); + const currentProjectServerNames = Object.keys(projectServers); + const globalServerNames = Object.keys(mcpUserServers || {}); + const enterpriseServerNames = Object.keys(mcpEnterpriseServers || {}); + + // Project Available MCP: servers available to current project + // This includes: Enterprise (highest priority) + Global + Project-specific + const projectAvailableEntries = []; + + // Add enterprise servers first (highest priority) + for (const [name, config] of Object.entries(mcpEnterpriseServers || {})) { + projectAvailableEntries.push({ + name, + config, + source: 'enterprise', + canRemove: false, + canToggle: false + }); + } + + // Add global servers + for (const [name, config] of Object.entries(mcpUserServers || {})) { + if (!enterpriseServerNames.includes(name)) { + projectAvailableEntries.push({ + name, + config, + source: 'global', + canRemove: false, // Can't remove from project view, must go to global management + canToggle: true, + isEnabled: !disabledServers.includes(name) + }); + } + } + + // Add project-specific servers + for (const [name, config] of Object.entries(projectServers)) { + if (!enterpriseServerNames.includes(name) && !globalServerNames.includes(name)) { + projectAvailableEntries.push({ + name, + config, + source: 'project', + canRemove: true, + canToggle: true, + isEnabled: !disabledServers.includes(name) + }); + } + } + + // Global Management: user global servers (for management) + const globalManagementEntries = Object.entries(mcpUserServers || {}); + + // Enterprise servers (for display only, read-only) + const enterpriseServerEntries = Object.entries(mcpEnterpriseServers || {}); + + // Other Projects: servers from other projects (not in current project, not global) const otherProjectServers = Object.entries(allAvailableServers) .filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal); // Check if CCW Tools is already installed @@ -126,20 +179,31 @@ async function renderMcpManager() {
    - +
    -

    ${t('mcp.currentProject')}

    +

    ${t('mcp.projectAvailable')}

    + ${hasMcpJson ? ` + + + .mcp.json + + ` : ` + + + Will use .mcp.json + + `}
    - ${currentProjectServerNames.length} ${t('mcp.serversConfigured')} + ${projectAvailableEntries.length} ${t('mcp.serversAvailable')}
    - ${currentProjectServerNames.length === 0 ? ` + ${projectAvailableEntries.length === 0 ? `

    ${t('empty.noMcpServers')}

    @@ -147,53 +211,43 @@ async function renderMcpManager() {
    ` : `
    - ${currentProjectServerNames.map(serverName => { - const serverConfig = projectServers[serverName]; - const isEnabled = !disabledServers.includes(serverName); - return renderMcpServerCard(serverName, serverConfig, isEnabled, true); + ${projectAvailableEntries.map(entry => { + return renderProjectAvailableServerCard(entry); }).join('')}
    `}
    - - ${enterpriseServerEntries.length > 0 ? ` -
    -
    + +
    +
    +
    - -

    Enterprise MCP Servers

    - Managed + +

    ${t('mcp.globalAvailable')}

    - ${enterpriseServerEntries.length} servers (read-only) +
    + ${globalManagementEntries.length} ${t('mcp.globalServersFrom')} +
    + ${globalManagementEntries.length === 0 ? ` +
    +
    +

    ${t('empty.noGlobalMcpServers')}

    +

    ${t('empty.globalServersHint')}

    +
    + ` : `
    - ${enterpriseServerEntries.map(([serverName, serverConfig]) => { - return renderEnterpriseServerCard(serverName, serverConfig); + ${globalManagementEntries.map(([serverName, serverConfig]) => { + return renderGlobalManagementCard(serverName, serverConfig); }).join('')}
    -
    - ` : ''} - - - ${userServerEntries.length > 0 ? ` -
    -
    -
    - -

    User MCP Servers

    -
    - ${userServerEntries.length} servers from ~/.claude.json -
    - -
    - ${userServerEntries.map(([serverName, serverConfig]) => { - return renderGlobalServerCard(serverName, serverConfig, 'user'); - }).join('')} -
    -
    - ` : ''} + `} +
    @@ -238,6 +292,7 @@ async function renderMcpManager() { const serverNames = Object.keys(servers); const isCurrentProject = path === currentPath; const enabledCount = serverNames.filter(s => !projectDisabled.includes(s)).length; + const projectHasMcpJson = config.hasMcpJson || false; return ` @@ -245,9 +300,10 @@ async function renderMcpManager() {
    ${isCurrentProject ? '' : ''}
    -
    - ${escapeHtml(path.split('\\').pop() || path)} - ${isCurrentProject ? `${t('mcp.current')}` : ''} +
    + ${escapeHtml(path.split('\\').pop() || path)} + ${isCurrentProject ? `${t('mcp.current')}` : ''} + ${projectHasMcpJson ? `` : ''}
    ${escapeHtml(path)}
    @@ -291,25 +347,40 @@ async function renderMcpManager() { if (typeof lucide !== 'undefined') lucide.createIcons(); } -function renderMcpServerCard(serverName, serverConfig, isEnabled, isInCurrentProject) { - const command = serverConfig.command || 'N/A'; - const args = serverConfig.args || []; - const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0; +// Render card for Project Available MCP (current project can use) +function renderProjectAvailableServerCard(entry) { + const { name, config, source, canRemove, canToggle, isEnabled } = entry; + const command = config.command || 'N/A'; + const args = config.args || []; + const hasEnv = config.env && Object.keys(config.env).length > 0; + + // Source badge + let sourceBadge = ''; + if (source === 'enterprise') { + sourceBadge = 'Enterprise'; + } else if (source === 'global') { + sourceBadge = 'Global'; + } else if (source === 'project') { + sourceBadge = 'Project'; + } return ` -
    +
    - ${isEnabled ? '' : ''} -

    ${escapeHtml(serverName)}

    + ${canToggle && isEnabled ? '' : ''} +

    ${escapeHtml(name)}

    + ${sourceBadge}
    - + ${canToggle ? ` + + ` : ''}
    @@ -326,20 +397,85 @@ function renderMcpServerCard(serverName, serverConfig, isEnabled, isInCurrentPro ${hasEnv ? `
    env - ${Object.keys(serverConfig.env).length} variables + ${Object.keys(config.env).length} variables
    ` : ''}
    - ${isInCurrentProject ? ` -
    +
    + + ${canRemove ? ` + ` : ''} +
    +
    + `; +} + +// Render card for Global Management (manage global servers) +function renderGlobalManagementCard(serverName, serverConfig) { + const command = serverConfig.command || serverConfig.url || 'N/A'; + const args = serverConfig.args || []; + const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0; + const serverType = serverConfig.type || 'stdio'; + + return ` +
    +
    +
    + +

    ${escapeHtml(serverName)}

    - ` : ''} +
    + +
    +
    + ${serverType === 'stdio' ? 'cmd' : 'url'} + ${escapeHtml(command)} +
    + ${args.length > 0 ? ` +
    + args + ${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''} +
    + ` : ''} + ${hasEnv ? ` +
    + env + ${Object.keys(serverConfig.env).length} variables +
    + ` : ''} +
    + ${t('mcp.availableToAll')} +
    +
    + +
    + + +
    `; } @@ -373,13 +509,26 @@ function renderAvailableServerCard(serverName, serverInfo) { ` : ''}
    - +
    + + +
    @@ -398,101 +547,21 @@ function renderAvailableServerCard(serverName, serverInfo) { ${sourceProjectName ? `• from ${escapeHtml(sourceProjectName)}` : ''}
    -
    - `; -} -function renderGlobalServerCard(serverName, serverConfig, source = 'user') { - const command = serverConfig.command || serverConfig.url || 'N/A'; - const args = serverConfig.args || []; - const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0; - const serverType = serverConfig.type || 'stdio'; - - return ` -
    -
    -
    - -

    ${escapeHtml(serverName)}

    - User -
    -
    - -
    -
    - ${serverType === 'stdio' ? 'cmd' : 'url'} - ${escapeHtml(command)} -
    - ${args.length > 0 ? ` -
    - args - ${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''} -
    - ` : ''} - ${hasEnv ? ` -
    - env - ${Object.keys(serverConfig.env).length} variables -
    - ` : ''} -
    - Available to all projects from ~/.claude.json -
    -
    `; } -function renderEnterpriseServerCard(serverName, serverConfig) { - const command = serverConfig.command || serverConfig.url || 'N/A'; - const args = serverConfig.args || []; - const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0; - const serverType = serverConfig.type || 'stdio'; - - return ` -
    -
    -
    - -

    ${escapeHtml(serverName)}

    - Enterprise - -
    - - Read-only - -
    - -
    -
    - ${serverType === 'stdio' ? 'cmd' : 'url'} - ${escapeHtml(command)} -
    - ${args.length > 0 ? ` -
    - args - ${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''} -
    - ` : ''} - ${hasEnv ? ` -
    - env - ${Object.keys(serverConfig.env).length} variables -
    - ` : ''} -
    - Managed by organization (highest priority) -
    -
    -
    - `; -} function attachMcpEventListeners() { // Toggle switches @@ -504,16 +573,22 @@ function attachMcpEventListeners() { }); }); - // Add buttons - use btn.dataset instead of e.target.dataset for event bubbling safety - document.querySelectorAll('.mcp-server-card button[data-action="add"]').forEach(btn => { + // Add from other projects (with scope selection) + document.querySelectorAll('.mcp-server-card button[data-action="add-from-other"]').forEach(btn => { btn.addEventListener('click', async (e) => { const serverName = btn.dataset.serverName; const serverConfig = JSON.parse(btn.dataset.serverConfig); - await copyMcpServerToProject(serverName, serverConfig); + const scope = btn.dataset.scope; // 'project' or 'global' + + if (scope === 'global') { + await addGlobalMcpServer(serverName, serverConfig); + } else { + await copyMcpServerToProject(serverName, serverConfig); + } }); }); - // Remove buttons - use btn.dataset instead of e.target.dataset for event bubbling safety + // Remove buttons (project-level) document.querySelectorAll('.mcp-server-card button[data-action="remove"]').forEach(btn => { btn.addEventListener('click', async (e) => { const serverName = btn.dataset.serverName; @@ -522,4 +597,24 @@ function attachMcpEventListeners() { } }); }); + + // Remove buttons (global-level) + document.querySelectorAll('.mcp-server-card button[data-action="remove-global"]').forEach(btn => { + btn.addEventListener('click', async (e) => { + const serverName = btn.dataset.serverName; + if (confirm(t('mcp.removeGlobalConfirm', { name: serverName }))) { + await removeGlobalMcpServer(serverName); + } + }); + }); + + // Copy install command buttons + document.querySelectorAll('.mcp-server-card button[data-action="copy-install-cmd"]').forEach(btn => { + btn.addEventListener('click', async (e) => { + const serverName = btn.dataset.serverName; + const serverConfig = JSON.parse(btn.dataset.serverConfig); + const scope = btn.dataset.scope || 'project'; + await copyMcpInstallCommand(serverName, serverConfig, scope); + }); + }); } diff --git a/ccw/src/templates/dashboard-js/views/rules-manager.js b/ccw/src/templates/dashboard-js/views/rules-manager.js index 19f0fa4b..5428384f 100644 --- a/ccw/src/templates/dashboard-js/views/rules-manager.js +++ b/ccw/src/templates/dashboard-js/views/rules-manager.js @@ -335,9 +335,482 @@ function editRule(ruleName, location) { } } +// ========== Create Rule Modal ========== +var ruleCreateState = { + location: 'project', + fileName: '', + subdirectory: '', + isConditional: false, + paths: [''], + content: '', + mode: 'input', + generationType: 'description', + description: '', + extractScope: '', + extractFocus: '' +}; + function openRuleCreateModal() { - // Open create modal (to be implemented with modal) - if (window.showToast) { - showToast(t('rules.createNotImplemented'), 'info'); + // Reset state + ruleCreateState = { + location: 'project', + fileName: '', + subdirectory: '', + isConditional: false, + paths: [''], + content: '', + mode: 'input', + generationType: 'description', + description: '', + extractScope: '', + extractFocus: '' + }; + + // Create modal HTML + const modalHtml = ` + + `; + + // Add to DOM + const modalContainer = document.createElement('div'); + modalContainer.id = 'ruleCreateModal'; + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + // Initialize Lucide icons + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +function closeRuleCreateModal(event) { + if (event && event.target !== event.currentTarget) return; + const modal = document.getElementById('ruleCreateModal'); + if (modal) modal.remove(); +} + +function selectRuleLocation(location) { + ruleCreateState.location = location; + // Re-render modal + closeRuleCreateModal(); + openRuleCreateModal(); +} + +function toggleRuleConditional() { + ruleCreateState.isConditional = !ruleCreateState.isConditional; + const pathsContainer = document.getElementById('rulePathsContainer'); + if (pathsContainer) { + pathsContainer.style.display = ruleCreateState.isConditional ? 'block' : 'none'; + } +} + +function addRulePath() { + ruleCreateState.paths.push(''); + // Re-render paths list + const pathsList = document.getElementById('rulePathsList'); + if (pathsList) { + const index = ruleCreateState.paths.length - 1; + const pathHtml = ` +
    + + +
    + `; + pathsList.insertAdjacentHTML('beforeend', pathHtml); + if (typeof lucide !== 'undefined') lucide.createIcons(); + } +} + +function removeRulePath(index) { + ruleCreateState.paths.splice(index, 1); + // Re-render paths list + closeRuleCreateModal(); + openRuleCreateModal(); +} + +function switchRuleCreateMode(mode) { + ruleCreateState.mode = mode; + + // Toggle visibility of sections + const generationTypeSection = document.getElementById('ruleGenerationTypeSection'); + const descriptionSection = document.getElementById('ruleDescriptionSection'); + const extractSection = document.getElementById('ruleExtractSection'); + const conditionalSection = document.getElementById('ruleConditionalSection'); + const contentSection = document.getElementById('ruleContentSection'); + + if (mode === 'cli-generate') { + if (generationTypeSection) generationTypeSection.style.display = 'block'; + if (conditionalSection) conditionalSection.style.display = 'none'; + if (contentSection) contentSection.style.display = 'none'; + + // Show appropriate generation section + if (ruleCreateState.generationType === 'description') { + if (descriptionSection) descriptionSection.style.display = 'block'; + if (extractSection) extractSection.style.display = 'none'; + } else { + if (descriptionSection) descriptionSection.style.display = 'none'; + if (extractSection) extractSection.style.display = 'block'; + } + } else { + if (generationTypeSection) generationTypeSection.style.display = 'none'; + if (descriptionSection) descriptionSection.style.display = 'none'; + if (extractSection) extractSection.style.display = 'none'; + if (conditionalSection) conditionalSection.style.display = 'block'; + if (contentSection) contentSection.style.display = 'block'; + } + + // Re-render modal to update button states + closeRuleCreateModal(); + openRuleCreateModal(); +} + +function switchRuleGenerationType(type) { + ruleCreateState.generationType = type; + + // Toggle visibility of generation sections + const descriptionSection = document.getElementById('ruleDescriptionSection'); + const extractSection = document.getElementById('ruleExtractSection'); + + if (type === 'description') { + if (descriptionSection) descriptionSection.style.display = 'block'; + if (extractSection) extractSection.style.display = 'none'; + } else if (type === 'extract') { + if (descriptionSection) descriptionSection.style.display = 'none'; + if (extractSection) extractSection.style.display = 'block'; + } +} + +async function createRule() { + const fileNameInput = document.getElementById('ruleFileName'); + const subdirectoryInput = document.getElementById('ruleSubdirectory'); + const contentInput = document.getElementById('ruleContent'); + const pathInputs = document.querySelectorAll('.rule-path-input'); + const descriptionInput = document.getElementById('ruleDescription'); + const extractScopeInput = document.getElementById('ruleExtractScope'); + const extractFocusInput = document.getElementById('ruleExtractFocus'); + + const fileName = fileNameInput ? fileNameInput.value.trim() : ruleCreateState.fileName; + const subdirectory = subdirectoryInput ? subdirectoryInput.value.trim() : ruleCreateState.subdirectory; + + // Validate file name + if (!fileName) { + if (window.showToast) { + showToast(t('rules.fileNameRequired'), 'error'); + } + return; + } + + if (!fileName.endsWith('.md')) { + if (window.showToast) { + showToast(t('rules.fileNameMustEndMd'), 'error'); + } + return; + } + + // Prepare request based on mode + let requestBody; + + if (ruleCreateState.mode === 'cli-generate') { + // CLI generation mode + const description = descriptionInput ? descriptionInput.value.trim() : ruleCreateState.description; + const extractScope = extractScopeInput ? extractScopeInput.value.trim() : ruleCreateState.extractScope; + const extractFocus = extractFocusInput ? extractFocusInput.value.trim() : ruleCreateState.extractFocus; + + // Validate based on generation type + if (ruleCreateState.generationType === 'description' && !description) { + if (window.showToast) { + showToast(t('rules.descriptionRequired'), 'error'); + } + return; + } + + if (ruleCreateState.generationType === 'extract' && !extractScope) { + if (window.showToast) { + showToast(t('rules.extractScopeRequired'), 'error'); + } + return; + } + + requestBody = { + mode: 'cli-generate', + fileName, + location: ruleCreateState.location, + subdirectory: subdirectory || undefined, + projectPath, + generationType: ruleCreateState.generationType, + description: ruleCreateState.generationType === 'description' ? description : undefined, + extractScope: ruleCreateState.generationType === 'extract' ? extractScope : undefined, + extractFocus: ruleCreateState.generationType === 'extract' ? extractFocus : undefined + }; + + // Show progress message + if (window.showToast) { + showToast(t('rules.cliGenerating'), 'info'); + } + } else { + // Manual input mode + const content = contentInput ? contentInput.value.trim() : ruleCreateState.content; + + // Collect paths from inputs + const paths = []; + if (ruleCreateState.isConditional && pathInputs) { + pathInputs.forEach(input => { + const path = input.value.trim(); + if (path) paths.push(path); + }); + } + + // Validate content + if (!content) { + if (window.showToast) { + showToast(t('rules.contentRequired'), 'error'); + } + return; + } + + requestBody = { + mode: 'input', + fileName, + content, + paths: paths.length > 0 ? paths : undefined, + location: ruleCreateState.location, + subdirectory: subdirectory || undefined, + projectPath + }; + } + + try { + const response = await fetch('/api/rules/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to create rule'); + } + + const result = await response.json(); + + // Close modal + closeRuleCreateModal(); + + // Reload rules data + await loadRulesData(); + renderRulesView(); + + // Show success message + if (window.showToast) { + showToast(t('rules.created', { name: result.fileName }), 'success'); + } + } catch (err) { + console.error('Failed to create rule:', err); + if (window.showToast) { + showToast(err.message || t('rules.createError'), 'error'); + } } } diff --git a/ccw/src/templates/dashboard-js/views/skills-manager.js b/ccw/src/templates/dashboard-js/views/skills-manager.js index 4315451d..ec49c04b 100644 --- a/ccw/src/templates/dashboard-js/views/skills-manager.js +++ b/ccw/src/templates/dashboard-js/views/skills-manager.js @@ -337,9 +337,471 @@ function editSkill(skillName, location) { } } +// ========== Create Skill Modal ========== +var skillCreateState = { + mode: 'import', // 'import' or 'cli-generate' + location: 'project', + sourcePath: '', + customName: '', + validationResult: null, + // CLI Generate mode fields + generationType: 'description', // 'description' or 'template' + description: '', + skillName: '' +}; + function openSkillCreateModal() { - // Open create modal (to be implemented with modal) - if (window.showToast) { - showToast(t('skills.createNotImplemented'), 'info'); + // Reset state + skillCreateState = { + mode: 'import', + location: 'project', + sourcePath: '', + customName: '', + validationResult: null, + generationType: 'description', + description: '', + skillName: '' + }; + + // Create modal HTML + const modalHtml = ` + + `; + + // Add to DOM + const modalContainer = document.createElement('div'); + modalContainer.id = 'skillCreateModal'; + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + // Initialize Lucide icons + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +function closeSkillCreateModal(event) { + if (event && event.target !== event.currentTarget) return; + const modal = document.getElementById('skillCreateModal'); + if (modal) modal.remove(); +} + +function selectSkillLocation(location) { + skillCreateState.location = location; + // Re-render modal + closeSkillCreateModal(); + openSkillCreateModal(); +} + +function switchSkillCreateMode(mode) { + skillCreateState.mode = mode; + // Re-render modal + closeSkillCreateModal(); + openSkillCreateModal(); +} + +function switchSkillGenerationType(type) { + skillCreateState.generationType = type; + // Re-render modal + closeSkillCreateModal(); + openSkillCreateModal(); +} + +function browseSkillFolder() { + // Use browser prompt for now (Phase 3 will implement file browser) + const path = prompt(t('skills.enterFolderPath'), skillCreateState.sourcePath); + if (path !== null) { + skillCreateState.sourcePath = path; + document.getElementById('skillSourcePath').value = path; + } +} + +async function validateSkillImport() { + const sourcePathInput = document.getElementById('skillSourcePath'); + const sourcePath = sourcePathInput ? sourcePathInput.value.trim() : skillCreateState.sourcePath; + + if (!sourcePath) { + showValidationResult({ valid: false, errors: [t('skills.sourceFolderRequired')], skillInfo: null }); + return; + } + + skillCreateState.sourcePath = sourcePath; + + // Show loading state + showValidationResult({ loading: true }); + + try { + const response = await fetch('/api/skills/validate-import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sourcePath }) + }); + + if (!response.ok) throw new Error('Validation request failed'); + + const result = await response.json(); + skillCreateState.validationResult = result; + showValidationResult(result); + } catch (err) { + console.error('Failed to validate skill:', err); + showValidationResult({ valid: false, errors: [t('skills.validationError')], skillInfo: null }); + } +} + +function showValidationResult(result) { + const container = document.getElementById('skillValidationResult'); + if (!container) return; + + if (result.loading) { + container.innerHTML = ` +
    + + ${t('skills.validating')} +
    + `; + if (typeof lucide !== 'undefined') lucide.createIcons(); + return; + } + + if (result.valid) { + container.innerHTML = ` +
    +
    + + ${t('skills.validSkill')} +
    +
    +
    ${t('skills.name')}: ${escapeHtml(result.skillInfo.name)}
    +
    ${t('skills.description')}: ${escapeHtml(result.skillInfo.description)}
    + ${result.skillInfo.version ? `
    ${t('skills.version')}: ${escapeHtml(result.skillInfo.version)}
    ` : ''} + ${result.skillInfo.supportingFiles && result.skillInfo.supportingFiles.length > 0 ? `
    ${t('skills.supportingFiles')}: ${result.skillInfo.supportingFiles.length} ${t('skills.files')}
    ` : ''} +
    +
    + `; + } else { + container.innerHTML = ` +
    +
    + + ${t('skills.invalidSkill')} +
    +
      + ${result.errors.map(error => `
    • • ${escapeHtml(error)}
    • `).join('')} +
    +
    + `; + } + + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +async function createSkill() { + if (skillCreateState.mode === 'import') { + // Import Mode Logic + const sourcePathInput = document.getElementById('skillSourcePath'); + const customNameInput = document.getElementById('skillCustomName'); + + const sourcePath = sourcePathInput ? sourcePathInput.value.trim() : skillCreateState.sourcePath; + const customName = customNameInput ? customNameInput.value.trim() : skillCreateState.customName; + + if (!sourcePath) { + if (window.showToast) { + showToast(t('skills.sourceFolderRequired'), 'error'); + } + return; + } + + // Validate first if not already validated + if (!skillCreateState.validationResult || !skillCreateState.validationResult.valid) { + if (window.showToast) { + showToast(t('skills.validateFirst'), 'error'); + } + return; + } + + try { + const response = await fetch('/api/skills/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + mode: 'import', + location: skillCreateState.location, + sourcePath, + skillName: customName || undefined, + projectPath + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to create skill'); + } + + const result = await response.json(); + + // Close modal + closeSkillCreateModal(); + + // Reload skills data + await loadSkillsData(); + renderSkillsView(); + + // Show success message + if (window.showToast) { + showToast(t('skills.created', { name: result.skillName }), 'success'); + } + } catch (err) { + console.error('Failed to create skill:', err); + if (window.showToast) { + showToast(err.message || t('skills.createError'), 'error'); + } + } + } else if (skillCreateState.mode === 'cli-generate') { + // CLI Generate Mode Logic + const skillNameInput = document.getElementById('skillGenerateName'); + const descriptionInput = document.getElementById('skillDescription'); + + const skillName = skillNameInput ? skillNameInput.value.trim() : skillCreateState.skillName; + const description = descriptionInput ? descriptionInput.value.trim() : skillCreateState.description; + + // Validation + if (!skillName) { + if (window.showToast) { + showToast(t('skills.skillNameRequired'), 'error'); + } + return; + } + + if (skillCreateState.generationType === 'description' && !description) { + if (window.showToast) { + showToast(t('skills.descriptionRequired'), 'error'); + } + return; + } + + try { + // Show generating progress toast + if (window.showToast) { + showToast(t('skills.generating'), 'info'); + } + + const response = await fetch('/api/skills/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + mode: 'cli-generate', + location: skillCreateState.location, + generationType: skillCreateState.generationType, + skillName, + description: skillCreateState.generationType === 'description' ? description : undefined, + projectPath + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to generate skill'); + } + + const result = await response.json(); + + // Close modal + closeSkillCreateModal(); + + // Reload skills data + await loadSkillsData(); + renderSkillsView(); + + // Show success message + if (window.showToast) { + showToast(t('skills.generated', { name: result.skillName }), 'success'); + } + } catch (err) { + console.error('Failed to generate skill:', err); + if (window.showToast) { + showToast(err.message || t('skills.generateError'), 'error'); + } + } } } diff --git a/ccw/src/templates/dashboard.html b/ccw/src/templates/dashboard.html index 43994197..8c779a62 100644 --- a/ccw/src/templates/dashboard.html +++ b/ccw/src/templates/dashboard.html @@ -434,6 +434,11 @@ Rules 0
  • + @@ -592,6 +597,14 @@ +
    + + +

    Choose where to save this MCP server configuration

    +
    ): Promise { + try { + // Check if venv exists + if (!existsSync(CODEXLENS_VENV)) { + return { success: false, error: 'CodexLens not installed (venv not found)' }; + } + + console.log('[CodexLens] Uninstalling CodexLens...'); + console.log(`[CodexLens] Removing directory: ${CODEXLENS_DATA_DIR}`); + + // Remove the entire .codexlens directory + const fs = await import('fs'); + fs.rmSync(CODEXLENS_DATA_DIR, { recursive: true, force: true }); + + // Reset bootstrap cache + bootstrapChecked = false; + bootstrapReady = false; + + console.log('[CodexLens] CodexLens uninstalled successfully'); + return { success: true, message: 'CodexLens uninstalled successfully' }; + } catch (err) { + return { success: false, error: `Failed to uninstall CodexLens: ${(err as Error).message}` }; + } +} + // Export for direct usage -export { ensureReady, executeCodexLens, checkVenvStatus, bootstrapVenv, checkSemanticStatus, installSemantic }; +export { ensureReady, executeCodexLens, checkVenvStatus, bootstrapVenv, checkSemanticStatus, installSemantic, uninstallCodexLens }; // Backward-compatible export for tests export const codexLensTool = { diff --git a/codex-lens/src/codex_lens.egg-info/PKG-INFO b/codex-lens/src/codex_lens.egg-info/PKG-INFO index 8088c11d..30f0e1b2 100644 --- a/codex-lens/src/codex_lens.egg-info/PKG-INFO +++ b/codex-lens/src/codex_lens.egg-info/PKG-INFO @@ -11,7 +11,10 @@ Requires-Dist: typer>=0.9 Requires-Dist: rich>=13 Requires-Dist: pydantic>=2.0 Requires-Dist: tree-sitter>=0.20 +Requires-Dist: tree-sitter-python>=0.25 +Requires-Dist: tree-sitter-javascript>=0.25 +Requires-Dist: tree-sitter-typescript>=0.23 Requires-Dist: pathspec>=0.11 Provides-Extra: semantic Requires-Dist: numpy>=1.24; extra == "semantic" -Requires-Dist: sentence-transformers>=2.2; extra == "semantic" +Requires-Dist: fastembed>=0.2; extra == "semantic" diff --git a/codex-lens/src/codex_lens.egg-info/SOURCES.txt b/codex-lens/src/codex_lens.egg-info/SOURCES.txt index 79fe9eb2..ec794c49 100644 --- a/codex-lens/src/codex_lens.egg-info/SOURCES.txt +++ b/codex-lens/src/codex_lens.egg-info/SOURCES.txt @@ -14,10 +14,37 @@ src/codexlens/cli/commands.py src/codexlens/cli/output.py src/codexlens/parsers/__init__.py src/codexlens/parsers/factory.py +src/codexlens/search/__init__.py +src/codexlens/search/chain_search.py src/codexlens/semantic/__init__.py src/codexlens/semantic/chunker.py +src/codexlens/semantic/code_extractor.py src/codexlens/semantic/embedder.py +src/codexlens/semantic/llm_enhancer.py src/codexlens/semantic/vector_store.py src/codexlens/storage/__init__.py +src/codexlens/storage/dir_index.py src/codexlens/storage/file_cache.py -src/codexlens/storage/sqlite_store.py \ No newline at end of file +src/codexlens/storage/index_tree.py +src/codexlens/storage/migration_manager.py +src/codexlens/storage/path_mapper.py +src/codexlens/storage/registry.py +src/codexlens/storage/sqlite_store.py +src/codexlens/storage/migrations/__init__.py +src/codexlens/storage/migrations/migration_001_normalize_keywords.py +tests/test_cli_output.py +tests/test_code_extractor.py +tests/test_config.py +tests/test_entities.py +tests/test_errors.py +tests/test_file_cache.py +tests/test_llm_enhancer.py +tests/test_parsers.py +tests/test_performance_optimizations.py +tests/test_search_comprehensive.py +tests/test_search_full_coverage.py +tests/test_search_performance.py +tests/test_semantic.py +tests/test_semantic_search.py +tests/test_storage.py +tests/test_vector_search_full.py \ No newline at end of file diff --git a/codex-lens/src/codex_lens.egg-info/requires.txt b/codex-lens/src/codex_lens.egg-info/requires.txt index 22b74a3b..b9c006e2 100644 --- a/codex-lens/src/codex_lens.egg-info/requires.txt +++ b/codex-lens/src/codex_lens.egg-info/requires.txt @@ -2,8 +2,11 @@ typer>=0.9 rich>=13 pydantic>=2.0 tree-sitter>=0.20 +tree-sitter-python>=0.25 +tree-sitter-javascript>=0.25 +tree-sitter-typescript>=0.23 pathspec>=0.11 [semantic] numpy>=1.24 -sentence-transformers>=2.2 +fastembed>=0.2 diff --git a/test-rules-cli-generation.md b/test-rules-cli-generation.md new file mode 100644 index 00000000..0969bfef --- /dev/null +++ b/test-rules-cli-generation.md @@ -0,0 +1,138 @@ +# Test: Rules CLI Generation Feature + +## Overview +This document demonstrates the CLI generation feature for Rules Manager. + +## API Usage + +### Endpoint +``` +POST /api/rules/create +``` + +### Mode: CLI Generation +Set `mode: 'cli-generate'` in the request body. + +## Generation Types + +### 1. From Description +Generate rule from natural language description: + +```json +{ + "mode": "cli-generate", + "generationType": "description", + "description": "Always use TypeScript strict mode and proper type annotations", + "fileName": "typescript-strict.md", + "location": "project", + "subdirectory": "", + "projectPath": "D:/Claude_dms3" +} +``` + +### 2. From Template +Generate rule from template type: + +```json +{ + "mode": "cli-generate", + "generationType": "template", + "templateType": "error-handling", + "fileName": "error-handling-rules.md", + "location": "project", + "subdirectory": "", + "projectPath": "D:/Claude_dms3" +} +``` + +### 3. From Code Extraction +Extract rules from existing codebase: + +```json +{ + "mode": "cli-generate", + "generationType": "extract", + "extractScope": "ccw/src/**/*.ts", + "extractFocus": "error handling, async patterns, type safety", + "fileName": "extracted-patterns.md", + "location": "project", + "subdirectory": "", + "projectPath": "D:/Claude_dms3" +} +``` + +## Response Format + +### Success Response +```json +{ + "success": true, + "fileName": "typescript-strict.md", + "location": "project", + "path": "D:/Claude_dms3/.claude/rules/typescript-strict.md", + "subdirectory": null, + "generatedContent": "# TypeScript Strict Mode\n\n...", + "executionId": "1734168000000-gemini" +} +``` + +### Error Response +```json +{ + "error": "CLI execution failed: ...", + "stderr": "..." +} +``` + +## Implementation Details + +### Function: `generateRuleViaCLI()` + +**Parameters:** +- `generationType`: 'description' | 'template' | 'extract' +- `description`: Rule description (for 'description' mode) +- `templateType`: Template type (for 'template' mode) +- `extractScope`: File pattern like 'src/**/*.ts' (for 'extract' mode) +- `extractFocus`: Focus areas like 'error handling, naming' (for 'extract' mode) +- `fileName`: Target file name (must end with .md) +- `location`: 'project' or 'user' +- `subdirectory`: Optional subdirectory within rules folder +- `projectPath`: Project root path + +**Process:** +1. Build CLI prompt based on generation type +2. Execute Gemini CLI with 10-minute timeout +3. Extract generated content from stdout +4. Create rule file using `createRule()` +5. Return result with execution ID + +### Prompt Templates + +#### Description Mode +``` +PURPOSE: Generate Claude Code memory rule from description +MODE: write +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-rigorous-style.txt) +``` + +#### Extract Mode +``` +PURPOSE: Extract coding rules from codebase +MODE: analysis +CONTEXT: @{extractScope} +RULES: $(cat ~/.claude/workflows/cli-templates/prompts/analysis/02-analyze-code-patterns.txt) +``` + +## Error Handling + +- CLI execution timeout: 10 minutes (600000ms) +- Empty content validation +- File existence checking +- Generation type validation + +## Integration + +The feature integrates with: +- **cli-executor.ts**: For Gemini CLI execution +- **createRule()**: For file creation +- **Rules Manager UI**: For user interface (to be implemented)