mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Implement CLAUDE.md Manager View with file tree, viewer, and metadata actions
- Added main JavaScript functionality for CLAUDE.md management including file loading, rendering, and editing capabilities. - Created a test HTML file to validate the functionality of the CLAUDE.md manager. - Introduced CLI generation examples and documentation for rules creation via CLI. - Enhanced error handling and notifications for file operations.
This commit is contained in:
22
.mcp.json
Normal file
22
.mcp.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
IMPLEMENTATION_SUMMARY.md
Normal file
190
IMPLEMENTATION_SUMMARY.md
Normal file
@@ -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
|
||||
202
MCP_OPTIMIZATION_SUMMARY.md
Normal file
202
MCP_OPTIMIZATION_SUMMARY.md
Normal file
@@ -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<string, string>;
|
||||
};
|
||||
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
|
||||
269
ccw/src/core/mcp-templates-db.ts
Normal file
269
ccw/src/core/mcp-templates-db.ts
Normal file
@@ -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<string, string>;
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
804
ccw/src/core/routes/claude-routes.ts
Normal file
804
ccw/src/core/routes/claude-routes.ts
Normal file
@@ -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<any>) => 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<string>();
|
||||
|
||||
// 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<boolean> {
|
||||
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;
|
||||
}
|
||||
@@ -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<boolean>
|
||||
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<boolean>
|
||||
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) => {
|
||||
|
||||
@@ -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<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
550
ccw/src/core/routes/mcp-routes.ts.backup
Normal file
550
ccw/src/core/routes/mcp-routes.ts.backup
Normal file
@@ -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<any>) => 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<boolean> {
|
||||
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;
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<http.Ser
|
||||
if (await handleCliRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Claude CLAUDE.md routes (/api/memory/claude/*)
|
||||
if (pathname.startsWith('/api/memory/claude/')) {
|
||||
if (await handleClaudeRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Memory routes (/api/memory/*)
|
||||
if (pathname.startsWith('/api/memory/')) {
|
||||
if (await handleMemoryRoutes(routeContext)) return;
|
||||
|
||||
@@ -1528,6 +1528,11 @@ code.ctx-meta-chip-value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-toast.warning {
|
||||
background: hsl(38 92% 50%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-toast.fade-out {
|
||||
animation: toastFadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@@ -214,3 +214,113 @@
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
CREATE MODAL STYLES (Skills & Rules)
|
||||
========================================== */
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* Modal Dialog */
|
||||
.modal-dialog {
|
||||
animation: slideUpModal 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUpModal {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Location Selection Buttons */
|
||||
.location-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.location-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Validation Result Animations */
|
||||
#skillValidationResult > 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;
|
||||
}
|
||||
}
|
||||
|
||||
759
ccw/src/templates/dashboard-css/13-claude-manager.css
Normal file
759
ccw/src/templates/dashboard-css/13-claude-manager.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -184,6 +184,9 @@ function renderCliStatus() {
|
||||
</button>`
|
||||
: `<button class="btn-sm btn-outline flex items-center gap-1" onclick="initCodexLensIndex()">
|
||||
<i data-lucide="database" class="w-3 h-3"></i> Init Index
|
||||
</button>
|
||||
<button class="btn-sm btn-outline flex items-center gap-1" onclick="uninstallCodexLens()">
|
||||
<i data-lucide="trash-2" class="w-3 h-3"></i> Uninstall
|
||||
</button>`
|
||||
}
|
||||
</div>
|
||||
@@ -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 = `
|
||||
<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i data-lucide="database" class="w-5 h-5 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Install CodexLens</h3>
|
||||
<p class="text-sm text-muted-foreground">Python-based code indexing engine</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-muted/50 rounded-lg p-4">
|
||||
<h4 class="font-medium mb-2">What will be installed:</h4>
|
||||
<ul class="text-sm space-y-2 text-muted-foreground">
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
|
||||
<span><strong>Python virtual environment</strong> - Isolated Python environment</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
|
||||
<span><strong>CodexLens package</strong> - Code indexing and search engine</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
|
||||
<span><strong>SQLite FTS5</strong> - Full-text search database</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bg-primary/5 border border-primary/20 rounded-lg p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<i data-lucide="info" class="w-4 h-4 text-primary mt-0.5"></i>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
<p class="font-medium text-foreground">Installation Location</p>
|
||||
<p class="mt-1"><code class="bg-muted px-1 rounded">~/.codexlens/venv</code></p>
|
||||
<p class="mt-1">First installation may take 2-3 minutes to download and setup Python packages.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="codexlensInstallProgress" class="hidden">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full"></div>
|
||||
<span class="text-sm" id="codexlensInstallStatus">Starting installation...</span>
|
||||
</div>
|
||||
<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div id="codexlensProgressBar" class="h-full bg-primary transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">
|
||||
<button class="btn-outline px-4 py-2" onclick="closeCodexLensInstallWizard()">Cancel</button>
|
||||
<button id="codexlensInstallBtn" class="btn-primary px-4 py-2" onclick="startCodexLensInstall()">
|
||||
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
|
||||
Install Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<span class="animate-pulse">Installing...</span>';
|
||||
|
||||
// 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 = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> 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 = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> 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 = `
|
||||
<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-full bg-destructive/10 flex items-center justify-center">
|
||||
<i data-lucide="trash-2" class="w-5 h-5 text-destructive"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Uninstall CodexLens</h3>
|
||||
<p class="text-sm text-muted-foreground">Remove CodexLens and all data</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-destructive/5 border border-destructive/20 rounded-lg p-4">
|
||||
<h4 class="font-medium text-destructive mb-2">What will be removed:</h4>
|
||||
<ul class="text-sm space-y-2 text-muted-foreground">
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>
|
||||
<span>Virtual environment at <code class="bg-muted px-1 rounded">~/.codexlens/venv</code></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>
|
||||
<span>All CodexLens indexed data and databases</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>
|
||||
<span>Configuration and semantic search models</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bg-warning/10 border border-warning/20 rounded-lg p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>
|
||||
<div class="text-sm">
|
||||
<p class="font-medium text-warning">Warning</p>
|
||||
<p class="text-muted-foreground">This action cannot be undone. All indexed code data will be permanently deleted.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="codexlensUninstallProgress" class="hidden">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="animate-spin w-5 h-5 border-2 border-destructive border-t-transparent rounded-full"></div>
|
||||
<span class="text-sm" id="codexlensUninstallStatus">Removing files...</span>
|
||||
</div>
|
||||
<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div id="codexlensUninstallProgressBar" class="h-full bg-destructive transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">
|
||||
<button class="btn-outline px-4 py-2" onclick="closeCodexLensUninstallWizard()">Cancel</button>
|
||||
<button id="codexlensUninstallBtn" class="btn-destructive px-4 py-2" onclick="startCodexLensUninstall()">
|
||||
<i data-lucide="trash-2" class="w-4 h-4 mr-2"></i>
|
||||
Uninstall
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<span class="animate-pulse">Uninstalling...</span>';
|
||||
|
||||
// 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 = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> 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 = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> 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) + '</option>';
|
||||
}).join('');
|
||||
|
||||
const fallbackOptions = '<option value="">None</option>' + availableTools.map(function(tool) {
|
||||
const fallbackOptions = '<option value="">' + t('semantic.none') + '</option>' + availableTools.map(function(tool) {
|
||||
return '<option value="' + tool + '"' + (llmEnhancementSettings.fallbackTool === tool ? ' selected' : '') + '>' +
|
||||
tool.charAt(0).toUpperCase() + tool.slice(1) + '</option>';
|
||||
}).join('');
|
||||
@@ -607,16 +971,16 @@ function openSemanticSettingsModal() {
|
||||
'<i data-lucide="sparkles" class="w-5 h-5 text-primary"></i>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<h3 class="text-lg font-semibold">Semantic Search Settings</h3>' +
|
||||
'<p class="text-sm text-muted-foreground">Configure LLM enhancement for semantic indexing</p>' +
|
||||
'<h3 class="text-lg font-semibold">' + t('semantic.settings') + '</h3>' +
|
||||
'<p class="text-sm text-muted-foreground">' + t('semantic.configDesc') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="space-y-4">' +
|
||||
'<div class="flex items-center justify-between p-4 bg-muted/50 rounded-lg">' +
|
||||
'<div>' +
|
||||
'<h4 class="font-medium flex items-center gap-2">' +
|
||||
'<i data-lucide="brain" class="w-4 h-4"></i>LLM Enhancement</h4>' +
|
||||
'<p class="text-sm text-muted-foreground mt-1">Use LLM to generate code summaries for better semantic search</p>' +
|
||||
'<i data-lucide="brain" class="w-4 h-4"></i>' + t('semantic.llmEnhancement') + '</h4>' +
|
||||
'<p class="text-sm text-muted-foreground mt-1">' + t('semantic.llmDesc') + '</p>' +
|
||||
'</div>' +
|
||||
'<label class="cli-toggle">' +
|
||||
'<input type="checkbox" id="llmEnhancementToggle" ' + (llmEnhancementSettings.enabled ? 'checked' : '') +
|
||||
@@ -628,29 +992,29 @@ function openSemanticSettingsModal() {
|
||||
'<div class="grid grid-cols-2 gap-4">' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2">' +
|
||||
'<i data-lucide="cpu" class="w-3 h-3 inline mr-1"></i>Primary LLM Tool</label>' +
|
||||
'<i data-lucide="cpu" class="w-3 h-3 inline mr-1"></i>' + t('semantic.primaryTool') + '</label>' +
|
||||
'<select class="cli-setting-select w-full" id="llmToolSelect" onchange="updateLlmTool(this.value)" ' + disabled + '>' + toolOptions + '</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2">' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3 inline mr-1"></i>Fallback Tool</label>' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3 inline mr-1"></i>' + t('semantic.fallbackTool') + '</label>' +
|
||||
'<select class="cli-setting-select w-full" id="llmFallbackSelect" onchange="updateLlmFallback(this.value)" ' + disabled + '>' + fallbackOptions + '</select>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="grid grid-cols-2 gap-4">' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2">' +
|
||||
'<i data-lucide="layers" class="w-3 h-3 inline mr-1"></i>Batch Size</label>' +
|
||||
'<i data-lucide="layers" class="w-3 h-3 inline mr-1"></i>' + t('semantic.batchSize') + '</label>' +
|
||||
'<select class="cli-setting-select w-full" id="llmBatchSelect" onchange="updateLlmBatchSize(this.value)" ' + disabled + '>' +
|
||||
'<option value="1"' + (llmEnhancementSettings.batchSize === 1 ? ' selected' : '') + '>1 file</option>' +
|
||||
'<option value="3"' + (llmEnhancementSettings.batchSize === 3 ? ' selected' : '') + '>3 files</option>' +
|
||||
'<option value="5"' + (llmEnhancementSettings.batchSize === 5 ? ' selected' : '') + '>5 files</option>' +
|
||||
'<option value="10"' + (llmEnhancementSettings.batchSize === 10 ? ' selected' : '') + '>10 files</option>' +
|
||||
'<option value="1"' + (llmEnhancementSettings.batchSize === 1 ? ' selected' : '') + '>1 ' + t('semantic.file') + '</option>' +
|
||||
'<option value="3"' + (llmEnhancementSettings.batchSize === 3 ? ' selected' : '') + '>3 ' + t('semantic.files') + '</option>' +
|
||||
'<option value="5"' + (llmEnhancementSettings.batchSize === 5 ? ' selected' : '') + '>5 ' + t('semantic.files') + '</option>' +
|
||||
'<option value="10"' + (llmEnhancementSettings.batchSize === 10 ? ' selected' : '') + '>10 ' + t('semantic.files') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-2">' +
|
||||
'<i data-lucide="clock" class="w-3 h-3 inline mr-1"></i>Timeout</label>' +
|
||||
'<i data-lucide="clock" class="w-3 h-3 inline mr-1"></i>' + t('semantic.timeout') + '</label>' +
|
||||
'<select class="cli-setting-select w-full" id="llmTimeoutSelect" onchange="updateLlmTimeout(this.value)" ' + disabled + '>' +
|
||||
'<option value="60000"' + (llmEnhancementSettings.timeoutMs === 60000 ? ' selected' : '') + '>1 min</option>' +
|
||||
'<option value="180000"' + (llmEnhancementSettings.timeoutMs === 180000 ? ' selected' : '') + '>3 min</option>' +
|
||||
@@ -664,26 +1028,110 @@ function openSemanticSettingsModal() {
|
||||
'<div class="flex items-start gap-2">' +
|
||||
'<i data-lucide="info" class="w-4 h-4 text-primary mt-0.5"></i>' +
|
||||
'<div class="text-sm text-muted-foreground">' +
|
||||
'<p>LLM enhancement generates code summaries and keywords for each file, improving semantic search accuracy.</p>' +
|
||||
'<p class="mt-1">Run <code class="bg-muted px-1 rounded">codex-lens enhance</code> after enabling to process existing files.</p>' +
|
||||
'<p>' + t('semantic.enhanceInfo') + '</p>' +
|
||||
'<p class="mt-1">' + t('semantic.enhanceCommand') + ' <code class="bg-muted px-1 rounded">codex-lens enhance</code> ' + t('semantic.enhanceAfterEnable') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="flex gap-2 pt-2">' +
|
||||
'<button class="btn-sm btn-outline flex items-center gap-1 flex-1" onclick="runEnhanceCommand()" ' + disabled + '>' +
|
||||
'<i data-lucide="zap" class="w-3 h-3"></i>Run Enhance Now</button>' +
|
||||
'<i data-lucide="zap" class="w-3 h-3"></i>' + t('semantic.runEnhanceNow') + '</button>' +
|
||||
'<button class="btn-sm btn-outline flex items-center gap-1 flex-1" onclick="viewEnhanceStatus()">' +
|
||||
'<i data-lucide="bar-chart-2" class="w-3 h-3"></i>View Status</button>' +
|
||||
'<i data-lucide="bar-chart-2" class="w-3 h-3"></i>' + t('semantic.viewStatus') + '</button>' +
|
||||
'</div>' +
|
||||
'<div class="border-t border-border my-4"></div>' +
|
||||
'<div>' +
|
||||
'<h4 class="font-medium mb-3 flex items-center gap-2">' +
|
||||
'<i data-lucide="search" class="w-4 h-4"></i>' + t('semantic.testSearch') + '</h4>' +
|
||||
'<div class="space-y-3">' +
|
||||
'<div>' +
|
||||
'<input type="text" id="semanticSearchInput" class="tool-config-input w-full" ' +
|
||||
'placeholder="' + t('semantic.searchPlaceholder') + '" />' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<button class="btn-sm btn-primary w-full" id="runSemanticSearchBtn">' +
|
||||
'<i data-lucide="search" class="w-3 h-3"></i> ' + t('semantic.runSearch') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="semanticSearchResults" class="hidden">' +
|
||||
'<div class="bg-muted/30 rounded-lg p-3 max-h-64 overflow-y-auto">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<p class="text-sm font-medium">' + t('codexlens.results') + ':</p>' +
|
||||
'<span id="semanticResultCount" class="text-xs text-muted-foreground"></span>' +
|
||||
'</div>' +
|
||||
'<pre id="semanticResultContent" class="text-xs font-mono whitespace-pre-wrap break-all"></pre>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">' +
|
||||
'<button class="btn-outline px-4 py-2" onclick="closeSemanticSettingsModal()">Close</button>' +
|
||||
'<button class="btn-outline px-4 py-2" onclick="closeSemanticSettingsModal()">' + t('semantic.close') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
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 = '<span class="animate-pulse">' + t('codexlens.searching') + '</span>';
|
||||
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 = '<i data-lucide="search" class="w-3 h-3"></i> ' + 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 = '<i data-lucide="search" class="w-3 h-3"></i> ' + 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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': '异常',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
764
ccw/src/templates/dashboard-js/views/claude-manager.js
Normal file
764
ccw/src/templates/dashboard-js/views/claude-manager.js
Normal file
@@ -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 = '<div class="claude-manager-view loading">' +
|
||||
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
|
||||
'<p>' + t('common.loading') + '</p>' +
|
||||
'</div>';
|
||||
|
||||
// Load data
|
||||
await loadClaudeFiles();
|
||||
|
||||
// Render layout
|
||||
container.innerHTML = '<div class="claude-manager-view">' +
|
||||
'<div class="claude-manager-header">' +
|
||||
'<div class="claude-manager-header-left">' +
|
||||
'<h2><i data-lucide="file-code" class="w-5 h-5"></i> ' + t('claudeManager.title') + '</h2>' +
|
||||
'<span class="file-count-badge">' + claudeFilesData.summary.totalFiles + ' ' + t('claudeManager.files') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="claude-manager-header-right">' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="showCreateFileDialog()">' +
|
||||
'<i data-lucide="file-plus" class="w-4 h-4"></i> ' + t('claude.createFile') +
|
||||
'</button>' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="refreshClaudeFiles()">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> ' + t('common.refresh') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="claude-manager-columns">' +
|
||||
'<div class="claude-manager-column left" id="claude-file-tree"></div>' +
|
||||
'<div class="claude-manager-column center" id="claude-file-viewer"></div>' +
|
||||
'<div class="claude-manager-column right" id="claude-file-metadata"></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// 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 = '<div class="file-tree">' +
|
||||
// Search Box
|
||||
'<div class="file-tree-search">' +
|
||||
'<input type="text" id="fileSearchInput" placeholder="' + t('claude.searchPlaceholder') + '" ' +
|
||||
'value="' + escapeHtml(searchQuery) + '" oninput="filterFileTree(this.value)">' +
|
||||
'<i data-lucide="search" class="w-4 h-4"></i>' +
|
||||
'</div>' +
|
||||
renderClaudeFilesTree() +
|
||||
'</div>'; // end file-tree
|
||||
|
||||
container.innerHTML = html;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderClaudeFilesTree() {
|
||||
var html = '<div class="file-tree-section">' +
|
||||
'<div class="file-tree-header" onclick="toggleTreeSection(\'user\')">' +
|
||||
'<i data-lucide="' + (fileTreeExpanded.user ? 'chevron-down' : 'chevron-right') + '" class="w-4 h-4"></i>' +
|
||||
'<i data-lucide="user" class="w-4 h-4 text-orange-500"></i>' +
|
||||
'<span>' + t('claudeManager.userLevel') + '</span>' +
|
||||
'<span class="file-count">' + (claudeFilesData.user.main ? 1 : 0) + '</span>' +
|
||||
'</div>';
|
||||
|
||||
if (fileTreeExpanded.user) {
|
||||
// User CLAUDE.md (only main file, no rules)
|
||||
if (claudeFilesData.user.main) {
|
||||
html += renderFileTreeItem(claudeFilesData.user.main, 1);
|
||||
} else {
|
||||
html += '<div class="file-tree-item empty" style="padding-left: 1.5rem;">' +
|
||||
'<i data-lucide="file-x" class="w-4 h-4"></i>' +
|
||||
'<span>' + t('claudeManager.noFile') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>'; // end user section
|
||||
|
||||
// Project section
|
||||
html += '<div class="file-tree-section">' +
|
||||
'<div class="file-tree-header" onclick="toggleTreeSection(\'project\')">' +
|
||||
'<i data-lucide="' + (fileTreeExpanded.project ? 'chevron-down' : 'chevron-right') + '" class="w-4 h-4"></i>' +
|
||||
'<i data-lucide="folder" class="w-4 h-4 text-green-500"></i>' +
|
||||
'<span>' + t('claudeManager.projectLevel') + '</span>' +
|
||||
'<span class="file-count">' + (claudeFilesData.project.main ? 1 : 0) + '</span>' +
|
||||
'</div>';
|
||||
|
||||
if (fileTreeExpanded.project) {
|
||||
// Project CLAUDE.md (only main file, no rules)
|
||||
if (claudeFilesData.project.main) {
|
||||
html += renderFileTreeItem(claudeFilesData.project.main, 1);
|
||||
} else {
|
||||
html += '<div class="file-tree-item empty" style="padding-left: 1.5rem;">' +
|
||||
'<i data-lucide="file-x" class="w-4 h-4"></i>' +
|
||||
'<span>' + t('claudeManager.noFile') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>'; // end project section
|
||||
|
||||
// Modules section
|
||||
html += '<div class="file-tree-section">' +
|
||||
'<div class="file-tree-header">' +
|
||||
'<i data-lucide="package" class="w-4 h-4 text-blue-500"></i>' +
|
||||
'<span>' + t('claudeManager.moduleLevel') + '</span>' +
|
||||
'<span class="file-count">' + claudeFilesData.modules.length + '</span>' +
|
||||
'</div>';
|
||||
|
||||
if (claudeFilesData.modules.length > 0) {
|
||||
claudeFilesData.modules.forEach(function (file) {
|
||||
html += renderFileTreeItem(file, 1);
|
||||
});
|
||||
} else {
|
||||
html += '<div class="file-tree-item empty" style="padding-left: 1.5rem;">' +
|
||||
'<i data-lucide="file-x" class="w-4 h-4"></i>' +
|
||||
'<span>' + t('claudeManager.noModules') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '</div>'; // 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 '<div class="file-tree-item' + (isSelected ? ' selected' : '') + '" ' +
|
||||
'onclick="selectClaudeFile(\'' + safeId + '\')" ' +
|
||||
'style="padding-left: ' + indentPx + 'rem;">' +
|
||||
'<i data-lucide="file-text" class="w-4 h-4"></i>' +
|
||||
'<span class="file-name">' + escapeHtml(file.name) + '</span>' +
|
||||
(file.parentDirectory ? '<span class="file-path-hint">' + escapeHtml(file.parentDirectory) + '</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
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 = '<div class="empty-state">' +
|
||||
'<i data-lucide="file-search" class="w-12 h-12 opacity-20"></i>' +
|
||||
'<p>' + t('claudeManager.selectFile') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="file-viewer">' +
|
||||
'<div class="file-viewer-header">' +
|
||||
'<h3>' + escapeHtml(selectedFile.name) + '</h3>' +
|
||||
'<div class="file-viewer-actions">' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="copyFileContent()" title="' + t('claude.copyContent') + '">' +
|
||||
'<i data-lucide="copy" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="toggleEditMode()" title="' + t('common.edit') + '">' +
|
||||
'<i data-lucide="' + (isEditMode ? 'eye' : 'edit-2') + '" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="file-viewer-content">' +
|
||||
(isEditMode ? renderEditor() : renderMarkdownContent(selectedFile.content || '')) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
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 '<div class="markdown-content">' + marked.parse(content) + '</div>';
|
||||
} catch (e) {
|
||||
console.error('Error rendering markdown with marked.js:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Enhanced basic rendering
|
||||
var html = escapeHtml(content);
|
||||
|
||||
// Headers
|
||||
html = html
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^#### (.*$)/gim, '<h4>$1</h4>');
|
||||
|
||||
// Inline formatting
|
||||
html = html
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||||
|
||||
// Task lists
|
||||
html = html
|
||||
.replace(/- \[ \] (.+)$/gim, '<li class="task-list-item"><input type="checkbox" disabled> $1</li>')
|
||||
.replace(/- \[x\] (.+)$/gim, '<li class="task-list-item"><input type="checkbox" disabled checked> $1</li>');
|
||||
|
||||
// Lists
|
||||
html = html.replace(/^- (.+)$/gim, '<li>$1</li>');
|
||||
html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
||||
|
||||
// Code blocks
|
||||
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, function(match, lang, code) {
|
||||
return '<pre><code class="language-' + (lang || 'plaintext') + '">' + code + '</code></pre>';
|
||||
});
|
||||
|
||||
// Line breaks
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
|
||||
return '<div class="markdown-content">' + html + '</div>';
|
||||
}
|
||||
|
||||
function renderEditor() {
|
||||
return '<textarea id="claudeFileEditor" class="file-editor" ' +
|
||||
'oninput="markDirty()">' +
|
||||
escapeHtml(selectedFile.content || '') +
|
||||
'</textarea>';
|
||||
}
|
||||
|
||||
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 = '<div class="empty-state">' +
|
||||
'<i data-lucide="info" class="w-8 h-8 opacity-20"></i>' +
|
||||
'<p>' + t('claudeManager.noMetadata') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div class="file-metadata">' +
|
||||
'<div class="metadata-section">' +
|
||||
'<h4>' + t('claudeManager.fileInfo') + '</h4>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.level') + '</span>' +
|
||||
'<span class="value">' + t('claudeManager.level_' + selectedFile.level) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.path') + '</span>' +
|
||||
'<span class="value path">' + escapeHtml(selectedFile.relativePath) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.size') + '</span>' +
|
||||
'<span class="value">' + formatFileSize(selectedFile.size) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.modified') + '</span>' +
|
||||
'<span class="value">' + formatDate(selectedFile.lastModified) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if (selectedFile.stats) {
|
||||
html += '<div class="metadata-section">' +
|
||||
'<h4>' + t('claudeManager.statistics') + '</h4>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.lines') + '</span>' +
|
||||
'<span class="value">' + selectedFile.stats.lines + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.words') + '</span>' +
|
||||
'<span class="value">' + selectedFile.stats.words + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.characters') + '</span>' +
|
||||
'<span class="value">' + selectedFile.stats.characters + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '<div class="metadata-section">' +
|
||||
'<h4>' + t('claudeManager.actions') + '</h4>';
|
||||
|
||||
if (isEditMode) {
|
||||
html += '<button class="btn btn-sm btn-primary full-width" onclick="saveClaudeFile()"' +
|
||||
(isDirty ? '' : ' disabled') + '>' +
|
||||
'<i data-lucide="save" class="w-4 h-4"></i> ' + t('common.save') +
|
||||
'</button>';
|
||||
html += '<button class="btn btn-sm btn-secondary full-width" onclick="toggleEditMode()">' +
|
||||
'<i data-lucide="x" class="w-4 h-4"></i> ' + t('common.cancel') +
|
||||
'</button>';
|
||||
} else {
|
||||
html += '<button class="btn btn-sm btn-secondary full-width" onclick="toggleEditMode()">' +
|
||||
'<i data-lucide="edit-2" class="w-4 h-4"></i> ' + t('common.edit') +
|
||||
'</button>';
|
||||
}
|
||||
|
||||
// Delete button (only for CLAUDE.md files, not in edit mode)
|
||||
if (!isEditMode && selectedFile.level !== 'file') {
|
||||
html += '<button class="btn btn-sm btn-danger full-width" onclick="confirmDeleteFile()">' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4"></i> ' + t('claude.deleteFile') +
|
||||
'</button>';
|
||||
}
|
||||
|
||||
html += '</div>'; // end actions section
|
||||
|
||||
// CLI Sync Panel
|
||||
html += '<div class="metadata-section cli-sync-panel">' +
|
||||
'<div class="panel-header">' +
|
||||
'<i data-lucide="sparkles" class="w-4 h-4"></i>' +
|
||||
'<span>' + (t('claude.cliSync') || 'CLI Auto-Sync') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="sync-config">' +
|
||||
'<label>' + (t('claude.tool') || 'Tool') + '</label>' +
|
||||
'<select id="cliToolSelect" class="sync-select">' +
|
||||
'<option value="gemini">Gemini</option>' +
|
||||
'<option value="qwen">Qwen</option>' +
|
||||
'</select>' +
|
||||
'<label>' + (t('claude.mode') || 'Mode') + '</label>' +
|
||||
'<select id="cliModeSelect" class="sync-select">' +
|
||||
'<option value="update">' + (t('claude.modeUpdate') || 'Update (Smart Merge)') + '</option>' +
|
||||
'<option value="generate">' + (t('claude.modeGenerate') || 'Generate (Full Replace)') + '</option>' +
|
||||
'<option value="append">' + (t('claude.modeAppend') || 'Append') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<button class="btn btn-sm btn-primary full-width sync-button" onclick="syncFileWithCLI()" id="cliSyncButton">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> ' +
|
||||
(t('claude.syncButton') || 'Sync with CLI') +
|
||||
'</button>' +
|
||||
'<div id="syncProgress" class="sync-progress" style="display:none;">' +
|
||||
'<i data-lucide="loader" class="w-4 h-4"></i>' +
|
||||
'<span id="syncProgressText">' + (t('claude.syncing') || 'Analyzing...') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>'; // end cli-sync-panel
|
||||
|
||||
html += '</div>'; // 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 = '<div class="modal-overlay" onclick="closeCreateDialog()">' +
|
||||
'<div class="create-dialog" onclick="event.stopPropagation()">' +
|
||||
'<h3>' + t('claude.createDialogTitle') + '</h3>' +
|
||||
'<div class="dialog-form">' +
|
||||
'<label>' + t('claude.selectLevel') + '</label>' +
|
||||
'<select id="createLevel" onchange="toggleModulePathInput(this.value)">' +
|
||||
'<option value="user">' + t('claude.levelUser') + '</option>' +
|
||||
'<option value="project">' + t('claude.levelProject') + '</option>' +
|
||||
'<option value="module">' + t('claude.levelModule') + '</option>' +
|
||||
'</select>' +
|
||||
'<label id="modulePathLabel" style="display:none;">' + t('claude.modulePath') + '</label>' +
|
||||
'<input id="modulePath" type="text" style="display:none;" placeholder="e.g., src/components">' +
|
||||
'<label>' + t('claude.selectTemplate') + '</label>' +
|
||||
'<select id="createTemplate">' +
|
||||
'<option value="default">' + t('claude.templateDefault') + '</option>' +
|
||||
'<option value="minimal">' + t('claude.templateMinimal') + '</option>' +
|
||||
'<option value="comprehensive">' + t('claude.templateComprehensive') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="dialog-buttons">' +
|
||||
'<button onclick="closeCreateDialog()" class="btn btn-sm btn-secondary">' + t('common.cancel') + '</button>' +
|
||||
'<button onclick="createNewFile()" class="btn btn-sm btn-primary">' + t('claude.createFile') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 = '<div class="tool-item ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '">' +
|
||||
var codexLensHtml = '<div class="tool-item clickable ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '" onclick="showCodexLensConfigModal()">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span></div>' +
|
||||
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span>' +
|
||||
'<i data-lucide="settings" class="w-3 h-3 tool-config-icon"></i></div>' +
|
||||
'<div class="tool-item-desc">' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(codexLensStatus.ready
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
|
||||
'<button class="btn-sm btn-outline" onclick="initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> ' + t('cli.initIndex') + '</button>'
|
||||
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> ' + t('cli.initIndex') + '</button>' +
|
||||
'<button class="btn-sm btn-outline btn-danger" onclick="event.stopPropagation(); uninstallCodexLens()"><i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('cli.uninstall') + '</button>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> ' + t('cli.install') + '</button>') +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> ' + t('cli.install') + '</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
@@ -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 =
|
||||
'<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">' +
|
||||
'<div class="p-6">' +
|
||||
'<div class="flex items-center gap-3 mb-4">' +
|
||||
'<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">' +
|
||||
'<i data-lucide="download" class="w-5 h-5 text-primary"></i>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<h3 class="text-lg font-semibold">Install ' + toolName.charAt(0).toUpperCase() + toolName.slice(1) + '</h3>' +
|
||||
'<p class="text-sm text-muted-foreground">' + (toolDescriptions[toolName] || 'CLI tool') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="space-y-4">' +
|
||||
'<div class="bg-muted/50 rounded-lg p-4">' +
|
||||
'<h4 class="font-medium mb-2">What will be installed:</h4>' +
|
||||
'<ul class="text-sm space-y-2 text-muted-foreground">' +
|
||||
'<li class="flex items-start gap-2">' +
|
||||
'<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>' +
|
||||
'<span><strong>NPM Package:</strong> <code class="bg-muted px-1 rounded">' + (toolPackages[toolName] || toolName) + '</code></span>' +
|
||||
'</li>' +
|
||||
'<li class="flex items-start gap-2">' +
|
||||
'<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>' +
|
||||
'<span><strong>Global installation</strong> - Available system-wide</span>' +
|
||||
'</li>' +
|
||||
'<li class="flex items-start gap-2">' +
|
||||
'<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>' +
|
||||
'<span><strong>CLI commands</strong> - Accessible from terminal</span>' +
|
||||
'</li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'<div class="bg-primary/5 border border-primary/20 rounded-lg p-3">' +
|
||||
'<div class="flex items-start gap-2">' +
|
||||
'<i data-lucide="info" class="w-4 h-4 text-primary mt-0.5"></i>' +
|
||||
'<div class="text-sm text-muted-foreground">' +
|
||||
'<p class="font-medium text-foreground">Installation Method</p>' +
|
||||
'<p class="mt-1">Uses <code class="bg-muted px-1 rounded">npm install -g</code></p>' +
|
||||
'<p class="mt-1">First installation may take 1-2 minutes depending on network speed.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="cliInstallProgress" class="hidden">' +
|
||||
'<div class="flex items-center gap-3">' +
|
||||
'<div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full"></div>' +
|
||||
'<span class="text-sm" id="cliInstallStatus">Starting installation...</span>' +
|
||||
'</div>' +
|
||||
'<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div id="cliInstallProgressBar" class="h-full bg-primary transition-all duration-300" style="width: 0%"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">' +
|
||||
'<button class="btn-outline px-4 py-2" onclick="closeCliInstallWizard()">Cancel</button>' +
|
||||
'<button id="cliInstallBtn" class="btn-primary px-4 py-2" onclick="startCliInstall(\'' + toolName + '\')">' +
|
||||
'<i data-lucide="download" class="w-4 h-4 mr-2"></i>' +
|
||||
'Install Now' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
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 = '<span class="animate-pulse">Installing...</span>';
|
||||
|
||||
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 = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> 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 = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> 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 =
|
||||
'<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">' +
|
||||
'<div class="p-6">' +
|
||||
'<div class="flex items-center gap-3 mb-4">' +
|
||||
'<div class="w-10 h-10 rounded-full bg-destructive/10 flex items-center justify-center">' +
|
||||
'<i data-lucide="trash-2" class="w-5 h-5 text-destructive"></i>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<h3 class="text-lg font-semibold">Uninstall ' + toolName.charAt(0).toUpperCase() + toolName.slice(1) + '</h3>' +
|
||||
'<p class="text-sm text-muted-foreground">Remove CLI tool from system</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="space-y-4">' +
|
||||
'<div class="bg-destructive/5 border border-destructive/20 rounded-lg p-4">' +
|
||||
'<h4 class="font-medium text-destructive mb-2">What will be removed:</h4>' +
|
||||
'<ul class="text-sm space-y-2 text-muted-foreground">' +
|
||||
'<li class="flex items-start gap-2">' +
|
||||
'<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>' +
|
||||
'<span>Global NPM package</span>' +
|
||||
'</li>' +
|
||||
'<li class="flex items-start gap-2">' +
|
||||
'<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>' +
|
||||
'<span>CLI commands and executables</span>' +
|
||||
'</li>' +
|
||||
'<li class="flex items-start gap-2">' +
|
||||
'<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>' +
|
||||
'<span>Tool configuration (if any)</span>' +
|
||||
'</li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'<div class="bg-warning/10 border border-warning/20 rounded-lg p-3">' +
|
||||
'<div class="flex items-start gap-2">' +
|
||||
'<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>' +
|
||||
'<div class="text-sm">' +
|
||||
'<p class="font-medium text-warning">Note</p>' +
|
||||
'<p class="text-muted-foreground">You can reinstall this tool anytime from the CLI Manager.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="cliUninstallProgress" class="hidden">' +
|
||||
'<div class="flex items-center gap-3">' +
|
||||
'<div class="animate-spin w-5 h-5 border-2 border-destructive border-t-transparent rounded-full"></div>' +
|
||||
'<span class="text-sm" id="cliUninstallStatus">Removing package...</span>' +
|
||||
'</div>' +
|
||||
'<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div id="cliUninstallProgressBar" class="h-full bg-destructive transition-all duration-300" style="width: 0%"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">' +
|
||||
'<button class="btn-outline px-4 py-2" onclick="closeCliUninstallWizard()">Cancel</button>' +
|
||||
'<button id="cliUninstallBtn" class="btn-destructive px-4 py-2" onclick="startCliUninstall(\'' + toolName + '\')">' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4 mr-2"></i>' +
|
||||
'Uninstall' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
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 = '<span class="animate-pulse">Uninstalling...</span>';
|
||||
|
||||
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 = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> 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 = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CodexLens Configuration Modal ==========
|
||||
async function showCodexLensConfigModal() {
|
||||
var loadingContent = '<div class="text-center py-8">' +
|
||||
'<div class="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full mx-auto mb-4"></div>' +
|
||||
'<p class="text-muted-foreground">' + t('codexlens.loadingConfig') + '</p>' +
|
||||
'</div>';
|
||||
|
||||
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 = '<div class="bg-destructive/10 border border-destructive/20 rounded-lg p-4">' +
|
||||
'<div class="flex items-start gap-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-5 h-5 text-destructive mt-0.5"></i>' +
|
||||
'<div>' +
|
||||
'<p class="font-medium text-destructive">Failed to load configuration</p>' +
|
||||
'<p class="text-sm text-muted-foreground mt-1">' + err.message + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
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 '<div class="tool-config-modal">' +
|
||||
// Status Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.status') + '</h4>' +
|
||||
'<div class="tool-config-badges">' +
|
||||
'<span class="badge ' + (isInstalled ? 'badge-success' : 'badge-muted') + '">' +
|
||||
'<i data-lucide="' + (isInstalled ? 'check-circle' : 'circle-dashed') + '" class="w-3 h-3"></i> ' +
|
||||
(isInstalled ? t('codexlens.installed') : t('codexlens.notInstalled')) +
|
||||
'</span>' +
|
||||
'<span class="badge badge-primary">' +
|
||||
'<i data-lucide="database" class="w-3 h-3"></i> ' + indexCount + ' ' + t('codexlens.indexes') +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
(currentWorkspace !== 'None'
|
||||
? '<div class="mt-3 p-3 bg-muted/30 rounded-lg">' +
|
||||
'<p class="text-sm text-muted-foreground mb-1">' + t('codexlens.currentWorkspace') + ':</p>' +
|
||||
'<p class="text-sm font-mono break-all">' + escapeHtml(currentWorkspace) + '</p>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
|
||||
// Index Storage Path Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.indexStoragePath') + ' <span class="text-muted">(' + t('codexlens.whereIndexesStored') + ')</span></h4>' +
|
||||
'<div class="space-y-3">' +
|
||||
'<div class="bg-muted/30 rounded-lg p-3">' +
|
||||
'<p class="text-sm text-muted-foreground mb-2">' + t('codexlens.currentPath') + ':</p>' +
|
||||
'<p class="text-sm font-mono break-all bg-background px-2 py-1 rounded border border-border">' +
|
||||
escapeHtml(indexDir) +
|
||||
'</p>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="text-sm font-medium mb-2 block">' + t('codexlens.newStoragePath') + ':</label>' +
|
||||
'<input type="text" id="indexDirInput" class="tool-config-input w-full" ' +
|
||||
'placeholder="' + t('codexlens.pathPlaceholder') + '" ' +
|
||||
'value="' + escapeHtml(indexDir) + '" />' +
|
||||
'<p class="text-xs text-muted-foreground mt-2">' +
|
||||
'<i data-lucide="info" class="w-3 h-3 inline"></i> ' +
|
||||
t('codexlens.pathInfo') +
|
||||
'</p>' +
|
||||
'</div>' +
|
||||
'<div class="bg-warning/10 border border-warning/20 rounded-lg p-3">' +
|
||||
'<div class="flex items-start gap-2">' +
|
||||
'<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>' +
|
||||
'<div class="text-sm">' +
|
||||
'<p class="font-medium text-warning">' + t('codexlens.migrationRequired') + '</p>' +
|
||||
'<p class="text-muted-foreground mt-1">' + t('codexlens.migrationWarning') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Actions Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.actions') + '</h4>' +
|
||||
'<div class="tool-config-actions">' +
|
||||
(isInstalled
|
||||
? '<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex()">' +
|
||||
'<i data-lucide="database" class="w-3 h-3"></i> ' + t('codexlens.initializeIndex') +
|
||||
'</button>' +
|
||||
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); cleanCodexLensIndexes()">' +
|
||||
'<i data-lucide="trash" class="w-3 h-3"></i> ' + t('codexlens.cleanAllIndexes') +
|
||||
'</button>' +
|
||||
'<button class="btn-sm btn-outline btn-danger" onclick="event.stopPropagation(); uninstallCodexLens()">' +
|
||||
'<i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('cli.uninstall') +
|
||||
'</button>'
|
||||
: '<button class="btn-sm btn-primary" onclick="event.stopPropagation(); installCodexLens()">' +
|
||||
'<i data-lucide="download" class="w-3 h-3"></i> ' + t('codexlens.installCodexLens') +
|
||||
'</button>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Test Search Section
|
||||
(isInstalled
|
||||
? '<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.testSearch') + ' <span class="text-muted">(' + t('codexlens.testFunctionality') + ')</span></h4>' +
|
||||
'<div class="space-y-3">' +
|
||||
'<div class="flex gap-2">' +
|
||||
'<select id="searchTypeSelect" class="tool-config-select flex-1">' +
|
||||
'<option value="search">' + t('codexlens.textSearch') + '</option>' +
|
||||
'<option value="search_files">' + t('codexlens.fileSearch') + '</option>' +
|
||||
'<option value="symbol">' + t('codexlens.symbolSearch') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<input type="text" id="searchQueryInput" class="tool-config-input w-full" ' +
|
||||
'placeholder="' + t('codexlens.searchPlaceholder') + '" />' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<button class="btn-sm btn-primary w-full" id="runSearchBtn">' +
|
||||
'<i data-lucide="search" class="w-3 h-3"></i> ' + t('codexlens.runSearch') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="searchResults" class="hidden">' +
|
||||
'<div class="bg-muted/30 rounded-lg p-3 max-h-64 overflow-y-auto">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<p class="text-sm font-medium">' + t('codexlens.results') + ':</p>' +
|
||||
'<span id="searchResultCount" class="text-xs text-muted-foreground"></span>' +
|
||||
'</div>' +
|
||||
'<pre id="searchResultContent" class="text-xs font-mono whitespace-pre-wrap break-all"></pre>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
|
||||
// Footer
|
||||
'<div class="tool-config-footer">' +
|
||||
'<button class="btn btn-outline" onclick="closeModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button class="btn btn-primary" id="saveCodexLensConfigBtn">' +
|
||||
'<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
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 = '<span class="animate-pulse">' + t('common.saving') + '</span>';
|
||||
|
||||
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 = '<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + 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 = '<span class="animate-pulse">' + t('codexlens.searching') + '</span>';
|
||||
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 = '<i data-lucide="search" class="w-3 h-3"></i> ' + 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 = '<i data-lucide="search" class="w-3 h-3"></i> ' + 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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Project MCP Servers -->
|
||||
<!-- Project Available MCP Servers -->
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.currentProject')}</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.projectAvailable')}</h3>
|
||||
<button class="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
|
||||
onclick="openMcpCreateModal()">
|
||||
<span>+</span> ${t('mcp.newServer')}
|
||||
onclick="openMcpCreateModal('project')">
|
||||
<span>+</span> ${t('mcp.newProjectServer')}
|
||||
</button>
|
||||
${hasMcpJson ? `
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs bg-success/10 text-success rounded-md border border-success/20">
|
||||
<i data-lucide="file-check" class="w-3.5 h-3.5"></i>
|
||||
.mcp.json
|
||||
</span>
|
||||
` : `
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs bg-muted text-muted-foreground rounded-md border border-border" title="New servers will create .mcp.json">
|
||||
<i data-lucide="file-plus" class="w-3.5 h-3.5"></i>
|
||||
Will use .mcp.json
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${currentProjectServerNames.length} ${t('mcp.serversConfigured')}</span>
|
||||
<span class="text-sm text-muted-foreground">${projectAvailableEntries.length} ${t('mcp.serversAvailable')}</span>
|
||||
</div>
|
||||
|
||||
${currentProjectServerNames.length === 0 ? `
|
||||
${projectAvailableEntries.length === 0 ? `
|
||||
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="plug" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">${t('empty.noMcpServers')}</p>
|
||||
@@ -147,53 +211,43 @@ async function renderMcpManager() {
|
||||
</div>
|
||||
` : `
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${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('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Enterprise MCP Servers (Managed) -->
|
||||
${enterpriseServerEntries.length > 0 ? `
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<!-- Global Available MCP Servers -->
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="building-2" class="w-5 h-5"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">Enterprise MCP Servers</h3>
|
||||
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Managed</span>
|
||||
<i data-lucide="globe" class="w-5 h-5 text-success"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.globalAvailable')}</h3>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${enterpriseServerEntries.length} servers (read-only)</span>
|
||||
<button class="px-3 py-1.5 text-sm bg-success text-success-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
|
||||
onclick="openMcpCreateModal('global')">
|
||||
<span>+</span> ${t('mcp.newGlobalServer')}
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${globalManagementEntries.length} ${t('mcp.globalServersFrom')}</span>
|
||||
</div>
|
||||
|
||||
${globalManagementEntries.length === 0 ? `
|
||||
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="globe" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">${t('empty.noGlobalMcpServers')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('empty.globalServersHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${enterpriseServerEntries.map(([serverName, serverConfig]) => {
|
||||
return renderEnterpriseServerCard(serverName, serverConfig);
|
||||
${globalManagementEntries.map(([serverName, serverConfig]) => {
|
||||
return renderGlobalManagementCard(serverName, serverConfig);
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- User MCP Servers -->
|
||||
${userServerEntries.length > 0 ? `
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">User MCP Servers</h3>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${userServerEntries.length} servers from ~/.claude.json</span>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${userServerEntries.map(([serverName, serverConfig]) => {
|
||||
return renderGlobalServerCard(serverName, serverConfig, 'user');
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Available MCP Servers from Other Projects -->
|
||||
<div class="mcp-section">
|
||||
@@ -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 `
|
||||
<tr class="border-b border-border last:border-b-0 ${isCurrentProject ? 'bg-primary/5' : 'hover:bg-hover/50'}">
|
||||
@@ -245,9 +300,10 @@ async function renderMcpManager() {
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="shrink-0">${isCurrentProject ? '<i data-lucide="map-pin" class="w-4 h-4 text-primary"></i>' : '<i data-lucide="folder" class="w-4 h-4"></i>'}</span>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-foreground truncate text-sm" title="${escapeHtml(path)}">
|
||||
${escapeHtml(path.split('\\').pop() || path)}
|
||||
${isCurrentProject ? `<span class="ml-2 text-xs text-primary font-medium">${t('mcp.current')}</span>` : ''}
|
||||
<div class="font-medium text-foreground truncate text-sm flex items-center gap-2" title="${escapeHtml(path)}">
|
||||
<span class="truncate">${escapeHtml(path.split('\\').pop() || path)}</span>
|
||||
${isCurrentProject ? `<span class="text-xs text-primary font-medium shrink-0">${t('mcp.current')}</span>` : ''}
|
||||
${projectHasMcpJson ? `<span class="shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-success/10 text-success rounded" title=".mcp.json detected"><i data-lucide="file-check" class="w-3 h-3"></i></span>` : ''}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground truncate">${escapeHtml(path)}</div>
|
||||
</div>
|
||||
@@ -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 = '<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Enterprise</span>';
|
||||
} else if (source === 'global') {
|
||||
sourceBadge = '<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">Global</span>';
|
||||
} else if (source === 'project') {
|
||||
sourceBadge = '<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Project</span>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${isEnabled ? '' : 'opacity-60'}">
|
||||
<div class="mcp-server-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${canToggle && !isEnabled ? 'opacity-60' : ''}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>${isEnabled ? '<i data-lucide="check-circle" class="w-5 h-5 text-success"></i>' : '<i data-lucide="x-circle" class="w-5 h-5 text-destructive"></i>'}</span>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span>${canToggle && isEnabled ? '<i data-lucide="check-circle" class="w-5 h-5 text-success"></i>' : '<i data-lucide="circle" class="w-5 h-5 text-muted-foreground"></i>'}</span>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(name)}</h4>
|
||||
${sourceBadge}
|
||||
</div>
|
||||
<label class="mcp-toggle relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer"
|
||||
${isEnabled ? 'checked' : ''}
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-action="toggle">
|
||||
<div class="w-9 h-5 bg-hover peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-success"></div>
|
||||
</label>
|
||||
${canToggle ? `
|
||||
<label class="mcp-toggle relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer"
|
||||
${isEnabled ? 'checked' : ''}
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-action="toggle">
|
||||
<div class="w-9 h-5 bg-hover peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-success"></div>
|
||||
</label>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
@@ -326,20 +397,85 @@ function renderMcpServerCard(serverName, serverConfig, isEnabled, isInCurrentPro
|
||||
${hasEnv ? `
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||
<span class="text-xs">${Object.keys(config.env).length} variables</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${isInCurrentProject ? `
|
||||
<div class="mt-3 pt-3 border-t border-border">
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between">
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(config))}"
|
||||
data-scope="${source === 'global' ? 'global' : 'project'}"
|
||||
data-action="copy-install-cmd">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
${t('mcp.copyInstallCmd')}
|
||||
</button>
|
||||
${canRemove ? `
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-action="remove">
|
||||
${t('mcp.removeFromProject')}
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 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 `
|
||||
<div class="mcp-server-card mcp-server-global bg-card border border-success/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="globe" class="w-5 h-5 text-success"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
|
||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||
</div>
|
||||
${args.length > 0 ? `
|
||||
<div class="flex items-start gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
||||
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${hasEnv ? `
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
||||
<span class="text-xs italic">${t('mcp.availableToAll')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between">
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
||||
data-scope="global"
|
||||
data-action="copy-install-cmd">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
${t('mcp.copyInstallCmd')}
|
||||
</button>
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-action="remove-global">
|
||||
${t('mcp.removeGlobal')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -373,13 +509,26 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-server-name="${escapeHtml(originalName)}"
|
||||
data-server-key="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-action="add">
|
||||
${t('mcp.add')}
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-server-name="${escapeHtml(originalName)}"
|
||||
data-server-key="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-scope="project"
|
||||
data-action="add-from-other"
|
||||
title="${t('mcp.installToProject')}">
|
||||
<i data-lucide="folder-plus" class="w-3.5 h-3.5 inline"></i>
|
||||
</button>
|
||||
<button class="px-3 py-1 text-xs bg-success text-success-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-server-name="${escapeHtml(originalName)}"
|
||||
data-server-key="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-scope="global"
|
||||
data-action="add-from-other"
|
||||
title="${t('mcp.installToGlobal')}">
|
||||
<i data-lucide="globe" class="w-3.5 h-3.5 inline"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
@@ -398,101 +547,21 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
||||
${sourceProjectName ? `<span class="text-xs text-muted-foreground/70">• from ${escapeHtml(sourceProjectName)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="mcp-server-card mcp-server-global bg-card border border-primary/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">User</span>
|
||||
</div>
|
||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-action="add">
|
||||
${t('mcp.addToProject')}
|
||||
<div class="mt-3 pt-3 border-t border-border">
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||
data-server-name="${escapeHtml(originalName)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
||||
data-scope="project"
|
||||
data-action="copy-install-cmd">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
${t('mcp.copyInstallCmd')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
|
||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||
</div>
|
||||
${args.length > 0 ? `
|
||||
<div class="flex items-start gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
||||
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${hasEnv ? `
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
||||
<span class="text-xs italic">Available to all projects from ~/.claude.json</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="mcp-server-card mcp-server-enterprise bg-card border border-warning/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="building-2" class="w-5 h-5"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Enterprise</span>
|
||||
<i data-lucide="lock" class="w-3 h-3 text-muted-foreground"></i>
|
||||
</div>
|
||||
<span class="px-3 py-1 text-xs bg-muted text-muted-foreground rounded cursor-not-allowed">
|
||||
Read-only
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
|
||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||
</div>
|
||||
${args.length > 0 ? `
|
||||
<div class="flex items-start gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
||||
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${hasEnv ? `
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
||||
<span class="text-xs italic">Managed by organization (highest priority)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
<div class="modal-overlay fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="closeRuleCreateModal(event)">
|
||||
<div class="modal-dialog bg-card rounded-lg shadow-lg w-full max-w-2xl max-h-[90vh] mx-4 flex flex-col" onclick="event.stopPropagation()">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('rules.createRule')}</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
|
||||
onclick="closeRuleCreateModal()">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-5">
|
||||
<!-- Location Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.location')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="location-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${ruleCreateState.location === 'project' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="selectRuleLocation('project')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="folder" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('rules.projectRules')}</div>
|
||||
<div class="text-xs text-muted-foreground">.claude/rules/</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="location-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${ruleCreateState.location === 'user' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="selectRuleLocation('user')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('rules.userRules')}</div>
|
||||
<div class="text-xs text-muted-foreground">~/.claude/rules/</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.createMode')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="mode-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${ruleCreateState.mode === 'input' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchRuleCreateMode('input')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="edit" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('rules.manualInput')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('rules.manualInputHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="mode-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${ruleCreateState.mode === 'cli-generate' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchRuleCreateMode('cli-generate')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="sparkles" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('rules.cliGenerate')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('rules.cliGenerateHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.fileName')}</label>
|
||||
<input type="text" id="ruleFileName"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="my-rule.md"
|
||||
value="${ruleCreateState.fileName}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.fileNameHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Subdirectory -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.subdirectory')} <span class="text-muted-foreground">${t('common.optional')}</span></label>
|
||||
<input type="text" id="ruleSubdirectory"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="category/subcategory"
|
||||
value="${ruleCreateState.subdirectory}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.subdirectoryHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- CLI Generation Type (CLI mode only) -->
|
||||
<div id="ruleGenerationTypeSection" style="display: ${ruleCreateState.mode === 'cli-generate' ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.generationType')}</label>
|
||||
<div class="flex gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="ruleGenType" value="description"
|
||||
class="w-4 h-4 text-primary bg-background border-border focus:ring-2 focus:ring-primary"
|
||||
${ruleCreateState.generationType === 'description' ? 'checked' : ''}
|
||||
onchange="switchRuleGenerationType('description')">
|
||||
<span class="text-sm">${t('rules.fromDescription')}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="ruleGenType" value="extract"
|
||||
class="w-4 h-4 text-primary bg-background border-border focus:ring-2 focus:ring-primary"
|
||||
${ruleCreateState.generationType === 'extract' ? 'checked' : ''}
|
||||
onchange="switchRuleGenerationType('extract')">
|
||||
<span class="text-sm">${t('rules.fromCodeExtract')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Input (CLI mode, description type) -->
|
||||
<div id="ruleDescriptionSection" style="display: ${ruleCreateState.mode === 'cli-generate' && ruleCreateState.generationType === 'description' ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.description')}</label>
|
||||
<textarea id="ruleDescription"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
rows="4"
|
||||
placeholder="${t('rules.descriptionPlaceholder')}">${ruleCreateState.description}</textarea>
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.descriptionHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Code Extract Options (CLI mode, extract type) -->
|
||||
<div id="ruleExtractSection" style="display: ${ruleCreateState.mode === 'cli-generate' && ruleCreateState.generationType === 'extract' ? 'block' : 'none'}">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.extractScope')}</label>
|
||||
<input type="text" id="ruleExtractScope"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary font-mono"
|
||||
placeholder="src/**/*.ts"
|
||||
value="${ruleCreateState.extractScope}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.extractScopeHint')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.extractFocus')}</label>
|
||||
<input type="text" id="ruleExtractFocus"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="naming, error-handling, state-management"
|
||||
value="${ruleCreateState.extractFocus}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.extractFocusHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conditional Rule Toggle (Manual mode only) -->
|
||||
<div id="ruleConditionalSection" style="display: ${ruleCreateState.mode === 'input' ? 'block' : 'none'}">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="ruleConditional"
|
||||
class="w-4 h-4 text-primary bg-background border-border rounded focus:ring-2 focus:ring-primary"
|
||||
${ruleCreateState.isConditional ? 'checked' : ''}
|
||||
onchange="toggleRuleConditional()">
|
||||
<span class="text-sm font-medium text-foreground">${t('rules.conditionalRule')}</span>
|
||||
</label>
|
||||
<p class="text-xs text-muted-foreground mt-1 ml-6">${t('rules.conditionalHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Path Conditions -->
|
||||
<div id="rulePathsContainer" style="display: ${ruleCreateState.isConditional ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.pathConditions')}</label>
|
||||
<div id="rulePathsList" class="space-y-2">
|
||||
${ruleCreateState.paths.map((path, index) => `
|
||||
<div class="flex gap-2">
|
||||
<input type="text" class="rule-path-input flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="src/**/*.ts"
|
||||
value="${path}"
|
||||
data-index="${index}">
|
||||
${index > 0 ? `
|
||||
<button class="px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
||||
onclick="removeRulePath(${index})">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button class="mt-2 px-3 py-1.5 text-sm text-primary hover:bg-primary/10 rounded-lg transition-colors flex items-center gap-1"
|
||||
onclick="addRulePath()">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
${t('rules.addPath')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content (Manual mode only) -->
|
||||
<div id="ruleContentSection" style="display: ${ruleCreateState.mode === 'input' ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.content')}</label>
|
||||
<textarea id="ruleContent"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary font-mono"
|
||||
rows="10"
|
||||
placeholder="${t('rules.contentPlaceholder')}">${ruleCreateState.content}</textarea>
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.contentHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
|
||||
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
onclick="closeRuleCreateModal()">
|
||||
${t('common.cancel')}
|
||||
</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
|
||||
onclick="createRule()">
|
||||
${t('rules.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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 = `
|
||||
<div class="flex gap-2">
|
||||
<input type="text" class="rule-path-input flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="src/**/*.ts"
|
||||
value=""
|
||||
data-index="${index}">
|
||||
<button class="px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
||||
onclick="removeRulePath(${index})">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
<div class="modal-overlay fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="closeSkillCreateModal(event)">
|
||||
<div class="modal-dialog bg-card rounded-lg shadow-lg w-full max-w-2xl mx-4" onclick="event.stopPropagation()">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('skills.createSkill')}</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
|
||||
onclick="closeSkillCreateModal()">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="p-6 space-y-5">
|
||||
<!-- Location Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.location')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="location-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.location === 'project' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="selectSkillLocation('project')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="folder" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('skills.projectSkills')}</div>
|
||||
<div class="text-xs text-muted-foreground">.claude/skills/</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="location-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.location === 'user' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="selectSkillLocation('user')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('skills.userSkills')}</div>
|
||||
<div class="text-xs text-muted-foreground">~/.claude/skills/</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.createMode')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="mode-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.mode === 'import' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchSkillCreateMode('import')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="folder-input" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('skills.importFolder')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.importFolderHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="mode-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.mode === 'cli-generate' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchSkillCreateMode('cli-generate')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="sparkles" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('skills.cliGenerate')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.cliGenerateHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Mode Content -->
|
||||
<div id="skillImportMode" style="display: ${skillCreateState.mode === 'import' ? 'block' : 'none'}">
|
||||
<!-- Source Folder Path -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.sourceFolder')}</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="skillSourcePath"
|
||||
class="flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="${t('skills.sourceFolderPlaceholder')}"
|
||||
value="${skillCreateState.sourcePath}">
|
||||
<button class="px-4 py-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors text-sm"
|
||||
onclick="browseSkillFolder()">
|
||||
<i data-lucide="folder-open" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.sourceFolderHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Custom Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.customName')} <span class="text-muted-foreground">${t('common.optional')}</span></label>
|
||||
<input type="text" id="skillCustomName"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="${t('skills.customNamePlaceholder')}"
|
||||
value="${skillCreateState.customName}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.customNameHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Validation Result -->
|
||||
<div id="skillValidationResult"></div>
|
||||
</div>
|
||||
|
||||
<!-- CLI Generate Mode Content -->
|
||||
<div id="skillCliGenerateMode" style="display: ${skillCreateState.mode === 'cli-generate' ? 'block' : 'none'}">
|
||||
<!-- Skill Name (Required for CLI Generate) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.skillName')} <span class="text-destructive">*</span></label>
|
||||
<input type="text" id="skillGenerateName"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="${t('skills.skillNamePlaceholder')}"
|
||||
value="${skillCreateState.skillName}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.skillNameHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Generation Type Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.generationType')}</label>
|
||||
<div class="flex gap-3">
|
||||
<button class="flex-1 px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.generationType === 'description' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchSkillGenerationType('description')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium text-sm">${t('skills.fromDescription')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.fromDescriptionHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex-1 px-4 py-3 text-left border-2 rounded-lg transition-all opacity-50 cursor-not-allowed"
|
||||
disabled>
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="layout-template" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium text-sm">${t('skills.fromTemplate')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.comingSoon')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Text Area (for 'description' type) -->
|
||||
<div id="skillDescriptionArea" style="display: ${skillCreateState.generationType === 'description' ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.descriptionLabel')} <span class="text-destructive">*</span></label>
|
||||
<textarea id="skillDescription"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="${t('skills.descriptionPlaceholder')}"
|
||||
rows="6">${skillCreateState.description}</textarea>
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.descriptionGenerateHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- CLI Generate Info -->
|
||||
<div class="p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
|
||||
<div class="flex items-start gap-2">
|
||||
<i data-lucide="info" class="w-4 h-4 text-blue-600 mt-0.5"></i>
|
||||
<div class="text-sm text-blue-600">
|
||||
<p class="font-medium">${t('skills.cliGenerateInfo')}</p>
|
||||
<p class="text-xs mt-1">${t('skills.cliGenerateTimeHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
|
||||
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
onclick="closeSkillCreateModal()">
|
||||
${t('common.cancel')}
|
||||
</button>
|
||||
${skillCreateState.mode === 'import' ? `
|
||||
<button class="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
|
||||
onclick="validateSkillImport()">
|
||||
${t('skills.validate')}
|
||||
</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
|
||||
onclick="createSkill()">
|
||||
${t('skills.import')}
|
||||
</button>
|
||||
` : `
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="createSkill()">
|
||||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||||
${t('skills.generate')}
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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 = `
|
||||
<div class="flex items-center gap-2 p-3 bg-muted/50 rounded-lg">
|
||||
<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>
|
||||
<span class="text-sm text-muted-foreground">${t('skills.validating')}</span>
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.valid) {
|
||||
container.innerHTML = `
|
||||
<div class="p-4 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||||
<div class="flex items-center gap-2 text-green-600 mb-2">
|
||||
<i data-lucide="check-circle" class="w-5 h-5"></i>
|
||||
<span class="font-medium">${t('skills.validSkill')}</span>
|
||||
</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div><span class="text-muted-foreground">${t('skills.name')}:</span> <span class="font-medium">${escapeHtml(result.skillInfo.name)}</span></div>
|
||||
<div><span class="text-muted-foreground">${t('skills.description')}:</span> <span>${escapeHtml(result.skillInfo.description)}</span></div>
|
||||
${result.skillInfo.version ? `<div><span class="text-muted-foreground">${t('skills.version')}:</span> <span>${escapeHtml(result.skillInfo.version)}</span></div>` : ''}
|
||||
${result.skillInfo.supportingFiles && result.skillInfo.supportingFiles.length > 0 ? `<div><span class="text-muted-foreground">${t('skills.supportingFiles')}:</span> <span>${result.skillInfo.supportingFiles.length} ${t('skills.files')}</span></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||||
<div class="flex items-center gap-2 text-destructive mb-2">
|
||||
<i data-lucide="x-circle" class="w-5 h-5"></i>
|
||||
<span class="font-medium">${t('skills.invalidSkill')}</span>
|
||||
</div>
|
||||
<ul class="space-y-1 text-sm">
|
||||
${result.errors.map(error => `<li class="text-destructive">• ${escapeHtml(error)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,6 +434,11 @@
|
||||
<span class="nav-text flex-1" data-i18n="nav.rules">Rules</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeRules">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="claude-manager" data-tooltip="CLAUDE.md Manager">
|
||||
<i data-lucide="file-code" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.claudeManager">CLAUDE.md</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeClaude">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -592,6 +597,14 @@
|
||||
<input type="text" id="mcpServerName" data-i18n-placeholder="mcp.serverNamePlaceholder" placeholder="e.g., my-mcp-server"
|
||||
class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-foreground mb-1"><span data-i18n="mcp.scope">Scope</span> <span class="text-destructive">*</span></label>
|
||||
<select id="mcpServerScope" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20">
|
||||
<option value="project" data-i18n="mcp.scopeProject">Project - Only this project</option>
|
||||
<option value="global" data-i18n="mcp.scopeGlobal">Global - All projects (~/. claude.json)</option>
|
||||
</select>
|
||||
<p class="text-xs text-muted-foreground mt-1">Choose where to save this MCP server configuration</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-foreground mb-1"><span data-i18n="mcp.command">Command</span> <span class="text-destructive">*</span></label>
|
||||
<input type="text" id="mcpServerCommand" data-i18n-placeholder="mcp.commandPlaceholder" placeholder="e.g., npx, uvx, node, python"
|
||||
|
||||
@@ -834,8 +834,37 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall CodexLens by removing the venv directory
|
||||
* @returns Uninstall result
|
||||
*/
|
||||
async function uninstallCodexLens(): Promise<BootstrapResult> {
|
||||
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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
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
|
||||
@@ -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
|
||||
|
||||
138
test-rules-cli-generation.md
Normal file
138
test-rules-cli-generation.md
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user