mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: add semantic graph design for static code analysis
- Introduced a comprehensive design document for a Code Semantic Graph aimed at enhancing static analysis capabilities. - Defined the architecture, core components, and implementation steps for analyzing function calls, data flow, and dependencies. - Included detailed specifications for nodes and edges in the graph, along with database schema for storage. - Outlined phases for implementation, technical challenges, success metrics, and application scenarios.
This commit is contained in:
@@ -77,7 +77,7 @@ function getMcpServersFromFile(filePath) {
|
||||
*/
|
||||
function addMcpServerToMcpJson(projectPath, serverName, serverConfig) {
|
||||
try {
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const normalizedPath = normalizePathForFileSystem(projectPath);
|
||||
const mcpJsonPath = join(normalizedPath, '.mcp.json');
|
||||
|
||||
// Read existing .mcp.json or create new structure
|
||||
@@ -115,7 +115,7 @@ function addMcpServerToMcpJson(projectPath, serverName, serverConfig) {
|
||||
*/
|
||||
function removeMcpServerFromMcpJson(projectPath, serverName) {
|
||||
try {
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const normalizedPath = normalizePathForFileSystem(projectPath);
|
||||
const mcpJsonPath = join(normalizedPath, '.mcp.json');
|
||||
|
||||
if (!existsSync(mcpJsonPath)) {
|
||||
@@ -238,22 +238,43 @@ function getMcpConfig() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize project path for .claude.json (Windows backslash format)
|
||||
* Normalize path to filesystem format (for accessing .mcp.json files)
|
||||
* Always uses forward slashes for cross-platform compatibility
|
||||
* @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]\\/)) {
|
||||
function normalizePathForFileSystem(path) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize project path to match existing format in .claude.json
|
||||
* Checks both forward slash and backslash formats to find existing entry
|
||||
* @param {string} path
|
||||
* @param {Object} claudeConfig - Optional existing config to check format
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeProjectPathForConfig(path, claudeConfig = null) {
|
||||
// IMPORTANT: Always normalize to forward slashes to prevent duplicate entries
|
||||
// (e.g., prevents both "D:/Claude_dms3" and "D:\\Claude_dms3")
|
||||
let normalizedForward = path.replace(/\\/g, '/');
|
||||
|
||||
// Handle /d/path format -> D:/path
|
||||
if (normalizedForward.match(/^\/[a-zA-Z]\//)) {
|
||||
normalizedForward = normalizedForward.charAt(1).toUpperCase() + ':' + normalizedForward.slice(2);
|
||||
}
|
||||
|
||||
// ALWAYS return forward slash format to prevent duplicates
|
||||
return normalizedForward;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle MCP server enabled/disabled
|
||||
* @param {string} projectPath
|
||||
@@ -270,7 +291,7 @@ function toggleMcpServerEnabled(projectPath, serverName, enable) {
|
||||
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
||||
|
||||
if (!config.projects || !config.projects[normalizedPath]) {
|
||||
return { error: `Project not found: ${normalizedPath}` };
|
||||
@@ -332,7 +353,7 @@ function addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyC
|
||||
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
||||
|
||||
// Create project entry if it doesn't exist
|
||||
if (!config.projects) {
|
||||
@@ -387,8 +408,8 @@ function addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyC
|
||||
*/
|
||||
function removeMcpServerFromProject(projectPath, serverName) {
|
||||
try {
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const mcpJsonPath = join(normalizedPath, '.mcp.json');
|
||||
const normalizedPathForFile = normalizePathForFileSystem(projectPath);
|
||||
const mcpJsonPath = join(normalizedPathForFile, '.mcp.json');
|
||||
|
||||
let removedFromMcpJson = false;
|
||||
let removedFromClaudeJson = false;
|
||||
@@ -409,6 +430,9 @@ function removeMcpServerFromProject(projectPath, serverName) {
|
||||
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
// Get normalized path that matches existing config format
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
||||
|
||||
if (config.projects && config.projects[normalizedPath]) {
|
||||
const projectConfig = config.projects[normalizedPath];
|
||||
|
||||
@@ -597,11 +621,13 @@ export async function handleMcpRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
// 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;
|
||||
const { projectPath, serverName, serverConfig, configType } = body;
|
||||
if (!projectPath || !serverName || !serverConfig) {
|
||||
return { error: 'projectPath, serverName, and serverConfig are required', status: 400 };
|
||||
}
|
||||
return addMcpServerToProject(projectPath, serverName, serverConfig);
|
||||
// configType: 'mcp' = use .mcp.json (default), 'claude' = use .claude.json
|
||||
const useLegacyConfig = configType === 'claude';
|
||||
return addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyConfig);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
269
ccw/src/core/routes/mcp-templates-db.ts
Normal file
269
ccw/src/core/routes/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;
|
||||
}
|
||||
}
|
||||
@@ -733,7 +733,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
}
|
||||
|
||||
try {
|
||||
const configPath = join(projectPath, '.claude', 'rules', 'active_memory.md');
|
||||
const configPath = join(projectPath, '.claude', 'CLAUDE.md');
|
||||
const configJsonPath = join(projectPath, '.claude', 'active_memory_config.json');
|
||||
const enabled = existsSync(configPath);
|
||||
let lastSync: string | null = null;
|
||||
@@ -784,16 +784,12 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
return;
|
||||
}
|
||||
|
||||
const rulesDir = join(projectPath, '.claude', 'rules');
|
||||
const claudeDir = join(projectPath, '.claude');
|
||||
const configPath = join(rulesDir, 'active_memory.md');
|
||||
const configPath = join(claudeDir, 'CLAUDE.md');
|
||||
const configJsonPath = join(claudeDir, 'active_memory_config.json');
|
||||
|
||||
if (enabled) {
|
||||
// Enable: Create directories and initial file
|
||||
if (!existsSync(rulesDir)) {
|
||||
mkdirSync(rulesDir, { recursive: true });
|
||||
}
|
||||
if (!existsSync(claudeDir)) {
|
||||
mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
@@ -803,8 +799,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
writeFileSync(configJsonPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
// Create initial active_memory.md with header
|
||||
const initialContent = `# Active Memory
|
||||
// Create initial CLAUDE.md with header
|
||||
const initialContent = `# CLAUDE.md - Project Memory
|
||||
|
||||
> Auto-generated understanding of frequently accessed files.
|
||||
> Last updated: ${new Date().toISOString()}
|
||||
@@ -867,7 +863,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Active Memory - Sync (analyze hot files using CLI and update active_memory.md)
|
||||
// API: Active Memory - Sync (analyze hot files using CLI and update CLAUDE.md)
|
||||
if (pathname === '/api/memory/active/sync' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||
@@ -882,8 +878,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
return;
|
||||
}
|
||||
|
||||
const rulesDir = join(projectPath, '.claude', 'rules');
|
||||
const configPath = join(rulesDir, 'active_memory.md');
|
||||
const claudeDir = join(projectPath, '.claude');
|
||||
const configPath = join(claudeDir, 'CLAUDE.md');
|
||||
|
||||
// Get hot files from memory store - with fallback
|
||||
let hotFiles: any[] = [];
|
||||
@@ -903,8 +899,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
return isAbsolute(filePath) ? filePath : join(projectPath, filePath);
|
||||
}).filter((p: string) => existsSync(p));
|
||||
|
||||
// Build the active memory content header
|
||||
let content = `# Active Memory
|
||||
// Build the CLAUDE.md content header
|
||||
let content = `# CLAUDE.md - Project Memory
|
||||
|
||||
> Auto-generated understanding of frequently accessed files using ${tool.toUpperCase()}.
|
||||
> Last updated: ${new Date().toISOString()}
|
||||
@@ -942,14 +938,29 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
|
||||
});
|
||||
|
||||
if (result.success && result.execution?.output) {
|
||||
// Extract stdout from output object
|
||||
cliOutput = typeof result.execution.output === 'string'
|
||||
? result.execution.output
|
||||
: result.execution.output.stdout || '';
|
||||
// Extract stdout from output object with proper serialization
|
||||
const output = result.execution.output;
|
||||
if (typeof output === 'string') {
|
||||
cliOutput = output;
|
||||
} else if (output && typeof output === 'object') {
|
||||
// Handle object output - extract stdout or serialize the object
|
||||
if (output.stdout && typeof output.stdout === 'string') {
|
||||
cliOutput = output.stdout;
|
||||
} else if (output.stderr && typeof output.stderr === 'string') {
|
||||
cliOutput = output.stderr;
|
||||
} else {
|
||||
// Last resort: serialize the entire object as JSON
|
||||
cliOutput = JSON.stringify(output, null, 2);
|
||||
}
|
||||
} else {
|
||||
cliOutput = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Add CLI output to content
|
||||
content += cliOutput + '\n\n---\n\n';
|
||||
// Add CLI output to content (only if not empty)
|
||||
if (cliOutput && cliOutput.trim()) {
|
||||
content += cliOutput + '\n\n---\n\n';
|
||||
}
|
||||
|
||||
} catch (cliErr) {
|
||||
// Fallback to basic analysis if CLI fails
|
||||
@@ -1007,8 +1018,8 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if (!existsSync(rulesDir)) {
|
||||
mkdirSync(rulesDir, { recursive: true });
|
||||
if (!existsSync(claudeDir)) {
|
||||
mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the file
|
||||
|
||||
Reference in New Issue
Block a user