mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Implement core memory management with knowledge graph and evolution tracking
- Added core-memory.js and core-memory-graph.js for managing core memory views and visualizations. - Introduced functions for viewing knowledge graphs and evolution history of memories. - Implemented modal dialogs for creating, editing, and viewing memory details. - Developed core-memory.ts for backend operations including list, import, export, and summary generation. - Integrated Zod for parameter validation in core memory operations. - Enhanced UI with dynamic rendering of memory cards and detailed views.
This commit is contained in:
555
ccw/src/core/core-memory-store.ts
Normal file
555
ccw/src/core/core-memory-store.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* Core Memory Store - Independent storage system for core memories
|
||||
* Provides persistent storage for high-level architectural and strategic context
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
|
||||
|
||||
// Types
|
||||
export interface CoreMemory {
|
||||
id: string; // Format: CMEM-YYYYMMDD-HHMMSS
|
||||
content: string;
|
||||
summary: string;
|
||||
raw_output?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
archived: boolean;
|
||||
metadata?: string; // JSON string
|
||||
}
|
||||
|
||||
export interface KnowledgeGraphNode {
|
||||
memory_id: string;
|
||||
node_id: string;
|
||||
node_type: string; // file, function, module, concept
|
||||
node_label: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeGraphEdge {
|
||||
memory_id: string;
|
||||
edge_source: string;
|
||||
edge_target: string;
|
||||
edge_type: string; // depends_on, implements, uses, relates_to
|
||||
}
|
||||
|
||||
export interface EvolutionVersion {
|
||||
memory_id: string;
|
||||
version: number;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
diff_stats?: {
|
||||
added: number;
|
||||
modified: number;
|
||||
deleted: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface KnowledgeGraph {
|
||||
nodes: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
}>;
|
||||
edges: Array<{
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core Memory Store using SQLite
|
||||
*/
|
||||
export class CoreMemoryStore {
|
||||
private db: Database.Database;
|
||||
private dbPath: string;
|
||||
private projectPath: string;
|
||||
|
||||
constructor(projectPath: string) {
|
||||
this.projectPath = projectPath;
|
||||
// Use centralized storage path
|
||||
const paths = StoragePaths.project(projectPath);
|
||||
const coreMemoryDir = join(paths.root, 'core-memory');
|
||||
ensureStorageDir(coreMemoryDir);
|
||||
|
||||
this.dbPath = join(coreMemoryDir, 'core_memory.db');
|
||||
this.db = new Database(this.dbPath);
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
this.db.pragma('synchronous = NORMAL');
|
||||
|
||||
this.initDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database schema
|
||||
*/
|
||||
private initDatabase(): void {
|
||||
this.db.exec(`
|
||||
-- Core memories table
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id TEXT PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
summary TEXT,
|
||||
raw_output TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
archived INTEGER DEFAULT 0,
|
||||
metadata TEXT
|
||||
);
|
||||
|
||||
-- Knowledge graph nodes table
|
||||
CREATE TABLE IF NOT EXISTS knowledge_graph (
|
||||
memory_id TEXT NOT NULL,
|
||||
node_id TEXT NOT NULL,
|
||||
node_type TEXT NOT NULL,
|
||||
node_label TEXT NOT NULL,
|
||||
PRIMARY KEY (memory_id, node_id),
|
||||
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Knowledge graph edges table
|
||||
CREATE TABLE IF NOT EXISTS knowledge_graph_edges (
|
||||
memory_id TEXT NOT NULL,
|
||||
edge_source TEXT NOT NULL,
|
||||
edge_target TEXT NOT NULL,
|
||||
edge_type TEXT NOT NULL,
|
||||
PRIMARY KEY (memory_id, edge_source, edge_target),
|
||||
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Evolution history table
|
||||
CREATE TABLE IF NOT EXISTS evolution_history (
|
||||
memory_id TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
diff_stats TEXT,
|
||||
PRIMARY KEY (memory_id, version),
|
||||
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Indexes for efficient queries
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_archived ON memories(archived);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_graph_memory ON knowledge_graph(memory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_graph_edges_memory ON knowledge_graph_edges(memory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_evolution_history_memory ON evolution_history(memory_id);
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate timestamp-based ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
return `CMEM-${year}${month}${day}-${hours}${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert a core memory
|
||||
*/
|
||||
upsertMemory(memory: Partial<CoreMemory> & { content: string }): CoreMemory {
|
||||
const now = new Date().toISOString();
|
||||
const id = memory.id || this.generateId();
|
||||
|
||||
// Check if memory exists
|
||||
const existingMemory = this.getMemory(id);
|
||||
|
||||
if (existingMemory) {
|
||||
// Update existing memory
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE memories
|
||||
SET content = ?, summary = ?, raw_output = ?, updated_at = ?, archived = ?, metadata = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
memory.content,
|
||||
memory.summary || existingMemory.summary,
|
||||
memory.raw_output || existingMemory.raw_output,
|
||||
now,
|
||||
memory.archived !== undefined ? (memory.archived ? 1 : 0) : existingMemory.archived ? 1 : 0,
|
||||
memory.metadata || existingMemory.metadata,
|
||||
id
|
||||
);
|
||||
|
||||
// Add evolution history entry
|
||||
const currentVersion = this.getLatestVersion(id);
|
||||
this.addEvolutionVersion(id, currentVersion + 1, memory.content);
|
||||
|
||||
return this.getMemory(id)!;
|
||||
} else {
|
||||
// Insert new memory
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO memories (id, content, summary, raw_output, created_at, updated_at, archived, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
id,
|
||||
memory.content,
|
||||
memory.summary || '',
|
||||
memory.raw_output || null,
|
||||
now,
|
||||
now,
|
||||
memory.archived ? 1 : 0,
|
||||
memory.metadata || null
|
||||
);
|
||||
|
||||
// Add initial evolution history entry (version 1)
|
||||
this.addEvolutionVersion(id, 1, memory.content);
|
||||
|
||||
return this.getMemory(id)!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory by ID
|
||||
*/
|
||||
getMemory(id: string): CoreMemory | null {
|
||||
const stmt = this.db.prepare(`SELECT * FROM memories WHERE id = ?`);
|
||||
const row = stmt.get(id) as any;
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
content: row.content,
|
||||
summary: row.summary,
|
||||
raw_output: row.raw_output,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
archived: Boolean(row.archived),
|
||||
metadata: row.metadata
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all memories
|
||||
*/
|
||||
getMemories(options: { archived?: boolean; limit?: number; offset?: number } = {}): CoreMemory[] {
|
||||
const { archived = false, limit = 50, offset = 0 } = options;
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM memories
|
||||
WHERE archived = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`);
|
||||
|
||||
const rows = stmt.all(archived ? 1 : 0, limit, offset) as any[];
|
||||
return rows.map(row => ({
|
||||
id: row.id,
|
||||
content: row.content,
|
||||
summary: row.summary,
|
||||
raw_output: row.raw_output,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
archived: Boolean(row.archived),
|
||||
metadata: row.metadata
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive a memory
|
||||
*/
|
||||
archiveMemory(id: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE memories
|
||||
SET archived = 1, updated_at = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
stmt.run(new Date().toISOString(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a memory
|
||||
*/
|
||||
deleteMemory(id: string): void {
|
||||
const stmt = this.db.prepare(`DELETE FROM memories WHERE id = ?`);
|
||||
stmt.run(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate summary for a memory using CLI tool
|
||||
*/
|
||||
async generateSummary(memoryId: string, tool: 'gemini' | 'qwen' = 'gemini'): Promise<string> {
|
||||
const memory = this.getMemory(memoryId);
|
||||
if (!memory) throw new Error('Memory not found');
|
||||
|
||||
// Import CLI executor
|
||||
const { executeCliTool } = await import('../tools/cli-executor.js');
|
||||
|
||||
const prompt = `
|
||||
PURPOSE: Generate a concise summary (2-3 sentences) of the following core memory content
|
||||
TASK: Extract key architectural decisions, strategic insights, and important context
|
||||
MODE: analysis
|
||||
EXPECTED: Plain text summary without markdown or formatting
|
||||
RULES: Be concise. Focus on high-level understanding. No technical jargon unless essential.
|
||||
|
||||
CONTENT:
|
||||
${memory.content}
|
||||
`;
|
||||
|
||||
const result = await executeCliTool({
|
||||
tool,
|
||||
prompt,
|
||||
mode: 'analysis',
|
||||
timeout: 60000,
|
||||
cd: this.projectPath,
|
||||
category: 'internal'
|
||||
});
|
||||
|
||||
const summary = result.stdout?.trim() || 'Failed to generate summary';
|
||||
|
||||
// Update memory with summary
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE memories
|
||||
SET summary = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
stmt.run(summary, new Date().toISOString(), memoryId);
|
||||
|
||||
// Add evolution history entry
|
||||
const currentVersion = this.getLatestVersion(memoryId);
|
||||
this.addEvolutionVersion(memoryId, currentVersion + 1, memory.content);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract knowledge graph from memory content
|
||||
*/
|
||||
extractKnowledgeGraph(memoryId: string): KnowledgeGraph {
|
||||
const memory = this.getMemory(memoryId);
|
||||
if (!memory) throw new Error('Memory not found');
|
||||
|
||||
// Simple extraction based on patterns in content
|
||||
const nodes: KnowledgeGraph['nodes'] = [];
|
||||
const edges: KnowledgeGraph['edges'] = [];
|
||||
const nodeSet = new Set<string>();
|
||||
|
||||
// Extract file references
|
||||
const filePattern = /(?:file|path|module):\s*([^\s,]+(?:\.ts|\.js|\.py|\.go|\.java|\.rs))/gi;
|
||||
let match;
|
||||
while ((match = filePattern.exec(memory.content)) !== null) {
|
||||
const filePath = match[1];
|
||||
if (!nodeSet.has(filePath)) {
|
||||
nodes.push({ id: filePath, type: 'file', label: filePath.split('/').pop() || filePath });
|
||||
nodeSet.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract function/class references
|
||||
const functionPattern = /(?:function|class|method):\s*(\w+)/gi;
|
||||
while ((match = functionPattern.exec(memory.content)) !== null) {
|
||||
const funcName = match[1];
|
||||
const nodeId = `func:${funcName}`;
|
||||
if (!nodeSet.has(nodeId)) {
|
||||
nodes.push({ id: nodeId, type: 'function', label: funcName });
|
||||
nodeSet.add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract module references
|
||||
const modulePattern = /(?:module|package):\s*(\w+(?:\/\w+)*)/gi;
|
||||
while ((match = modulePattern.exec(memory.content)) !== null) {
|
||||
const moduleName = match[1];
|
||||
const nodeId = `module:${moduleName}`;
|
||||
if (!nodeSet.has(nodeId)) {
|
||||
nodes.push({ id: nodeId, type: 'module', label: moduleName });
|
||||
nodeSet.add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract relationships
|
||||
const dependsPattern = /(\w+)\s+depends on\s+(\w+)/gi;
|
||||
while ((match = dependsPattern.exec(memory.content)) !== null) {
|
||||
const source = match[1];
|
||||
const target = match[2];
|
||||
edges.push({ source, target, type: 'depends_on' });
|
||||
}
|
||||
|
||||
const usesPattern = /(\w+)\s+uses\s+(\w+)/gi;
|
||||
while ((match = usesPattern.exec(memory.content)) !== null) {
|
||||
const source = match[1];
|
||||
const target = match[2];
|
||||
edges.push({ source, target, type: 'uses' });
|
||||
}
|
||||
|
||||
// Save to database
|
||||
this.db.prepare(`DELETE FROM knowledge_graph WHERE memory_id = ?`).run(memoryId);
|
||||
this.db.prepare(`DELETE FROM knowledge_graph_edges WHERE memory_id = ?`).run(memoryId);
|
||||
|
||||
const nodeStmt = this.db.prepare(`
|
||||
INSERT INTO knowledge_graph (memory_id, node_id, node_type, node_label)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (const node of nodes) {
|
||||
nodeStmt.run(memoryId, node.id, node.type, node.label);
|
||||
}
|
||||
|
||||
const edgeStmt = this.db.prepare(`
|
||||
INSERT INTO knowledge_graph_edges (memory_id, edge_source, edge_target, edge_type)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (const edge of edges) {
|
||||
edgeStmt.run(memoryId, edge.source, edge.target, edge.type);
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get knowledge graph for a memory
|
||||
*/
|
||||
getKnowledgeGraph(memoryId: string): KnowledgeGraph {
|
||||
const nodeStmt = this.db.prepare(`
|
||||
SELECT node_id, node_type, node_label
|
||||
FROM knowledge_graph
|
||||
WHERE memory_id = ?
|
||||
`);
|
||||
|
||||
const edgeStmt = this.db.prepare(`
|
||||
SELECT edge_source, edge_target, edge_type
|
||||
FROM knowledge_graph_edges
|
||||
WHERE memory_id = ?
|
||||
`);
|
||||
|
||||
const nodeRows = nodeStmt.all(memoryId) as any[];
|
||||
const edgeRows = edgeStmt.all(memoryId) as any[];
|
||||
|
||||
const nodes = nodeRows.map(row => ({
|
||||
id: row.node_id,
|
||||
type: row.node_type,
|
||||
label: row.node_label
|
||||
}));
|
||||
|
||||
const edges = edgeRows.map(row => ({
|
||||
source: row.edge_source,
|
||||
target: row.edge_target,
|
||||
type: row.edge_type
|
||||
}));
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest version number for a memory
|
||||
*/
|
||||
private getLatestVersion(memoryId: string): number {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT MAX(version) as max_version
|
||||
FROM evolution_history
|
||||
WHERE memory_id = ?
|
||||
`);
|
||||
const result = stmt.get(memoryId) as { max_version: number | null };
|
||||
return result.max_version || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add evolution version
|
||||
*/
|
||||
private addEvolutionVersion(memoryId: string, version: number, content: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO evolution_history (memory_id, version, content, timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(memoryId, version, content, new Date().toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Track evolution history
|
||||
*/
|
||||
trackEvolution(memoryId: string): EvolutionVersion[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT version, content, timestamp, diff_stats
|
||||
FROM evolution_history
|
||||
WHERE memory_id = ?
|
||||
ORDER BY version ASC
|
||||
`);
|
||||
|
||||
const rows = stmt.all(memoryId) as any[];
|
||||
return rows.map((row, index) => {
|
||||
let diffStats: EvolutionVersion['diff_stats'];
|
||||
|
||||
if (index > 0) {
|
||||
const prevContent = rows[index - 1].content;
|
||||
const currentContent = row.content;
|
||||
|
||||
// Simple diff calculation
|
||||
const prevLines = prevContent.split('\n');
|
||||
const currentLines = currentContent.split('\n');
|
||||
|
||||
let added = 0;
|
||||
let deleted = 0;
|
||||
let modified = 0;
|
||||
|
||||
const maxLen = Math.max(prevLines.length, currentLines.length);
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
const prevLine = prevLines[i];
|
||||
const currentLine = currentLines[i];
|
||||
|
||||
if (!prevLine && currentLine) added++;
|
||||
else if (prevLine && !currentLine) deleted++;
|
||||
else if (prevLine !== currentLine) modified++;
|
||||
}
|
||||
|
||||
diffStats = { added, modified, deleted };
|
||||
}
|
||||
|
||||
return {
|
||||
memory_id: memoryId,
|
||||
version: row.version,
|
||||
content: row.content,
|
||||
timestamp: row.timestamp,
|
||||
diff_stats: diffStats
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
close(): void {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance cache
|
||||
const storeCache = new Map<string, CoreMemoryStore>();
|
||||
|
||||
/**
|
||||
* Get or create a store instance for a project
|
||||
*/
|
||||
export function getCoreMemoryStore(projectPath: string): CoreMemoryStore {
|
||||
const normalizedPath = projectPath.toLowerCase().replace(/\\/g, '/');
|
||||
|
||||
if (!storeCache.has(normalizedPath)) {
|
||||
storeCache.set(normalizedPath, new CoreMemoryStore(projectPath));
|
||||
}
|
||||
return storeCache.get(normalizedPath)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all store instances
|
||||
*/
|
||||
export function closeAllStores(): void {
|
||||
const stores = Array.from(storeCache.values());
|
||||
for (const store of stores) {
|
||||
store.close();
|
||||
}
|
||||
storeCache.clear();
|
||||
}
|
||||
|
||||
export default CoreMemoryStore;
|
||||
@@ -30,7 +30,9 @@ const MODULE_CSS_FILES = [
|
||||
'12-skills-rules.css',
|
||||
'13-claude-manager.css',
|
||||
'14-graph-explorer.css',
|
||||
'15-mcp-manager.css'
|
||||
'15-mcp-manager.css',
|
||||
'16-help.css',
|
||||
'17-core-memory.css'
|
||||
];
|
||||
|
||||
const MODULE_FILES = [
|
||||
@@ -56,6 +58,8 @@ const MODULE_FILES = [
|
||||
'components/mcp-manager.js',
|
||||
'components/hook-manager.js',
|
||||
'components/version-check.js',
|
||||
'components/storage-manager.js',
|
||||
'components/index-manager.js',
|
||||
'views/home.js',
|
||||
'views/project-overview.js',
|
||||
'views/session-detail.js',
|
||||
@@ -69,6 +73,14 @@ const MODULE_FILES = [
|
||||
'views/hook-manager.js',
|
||||
'views/history.js',
|
||||
'views/graph-explorer.js',
|
||||
'views/memory.js',
|
||||
'views/core-memory.js',
|
||||
'views/core-memory-graph.js',
|
||||
'views/prompt-history.js',
|
||||
'views/skills-manager.js',
|
||||
'views/rules-manager.js',
|
||||
'views/claude-manager.js',
|
||||
'views/help.js',
|
||||
'main.js'
|
||||
];
|
||||
|
||||
|
||||
@@ -800,5 +800,127 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get Chinese response setting status
|
||||
if (pathname === '/api/language/chinese-response' && req.method === 'GET') {
|
||||
try {
|
||||
const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md');
|
||||
const chineseRefPattern = /@.*chinese-response\.md/i;
|
||||
|
||||
let enabled = false;
|
||||
let guidelinesPath = '';
|
||||
|
||||
// Check if user CLAUDE.md exists and contains Chinese response reference
|
||||
if (existsSync(userClaudePath)) {
|
||||
const content = readFileSync(userClaudePath, 'utf8');
|
||||
enabled = chineseRefPattern.test(content);
|
||||
}
|
||||
|
||||
// Find guidelines file path (project or user level)
|
||||
const projectGuidelinesPath = join(initialPath, '.claude', 'workflows', 'chinese-response.md');
|
||||
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md');
|
||||
|
||||
if (existsSync(projectGuidelinesPath)) {
|
||||
guidelinesPath = projectGuidelinesPath;
|
||||
} else if (existsSync(userGuidelinesPath)) {
|
||||
guidelinesPath = userGuidelinesPath;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
enabled,
|
||||
guidelinesPath,
|
||||
guidelinesExists: !!guidelinesPath,
|
||||
userClaudeMdExists: existsSync(userClaudePath)
|
||||
}));
|
||||
return true;
|
||||
} catch (error) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// API: Toggle Chinese response setting
|
||||
if (pathname === '/api/language/chinese-response' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const { enabled } = body;
|
||||
|
||||
if (typeof enabled !== 'boolean') {
|
||||
return { error: 'Missing or invalid enabled parameter', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md');
|
||||
const userClaudeDir = join(homedir(), '.claude');
|
||||
|
||||
// Find guidelines file path
|
||||
const projectGuidelinesPath = join(initialPath, '.claude', 'workflows', 'chinese-response.md');
|
||||
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md');
|
||||
|
||||
let guidelinesRef = '';
|
||||
if (existsSync(projectGuidelinesPath)) {
|
||||
// Use project-level guidelines with absolute path
|
||||
guidelinesRef = projectGuidelinesPath.replace(/\\/g, '/');
|
||||
} else if (existsSync(userGuidelinesPath)) {
|
||||
// Use user-level guidelines with ~ shorthand
|
||||
guidelinesRef = '~/.claude/workflows/chinese-response.md';
|
||||
} else {
|
||||
return { error: 'Chinese response guidelines file not found', status: 404 };
|
||||
}
|
||||
|
||||
const chineseRefLine = `- **中文回复准则**: @${guidelinesRef}`;
|
||||
const chineseRefPattern = /^- \*\*中文回复准则\*\*:.*chinese-response\.md.*$/gm;
|
||||
|
||||
// Ensure user .claude directory exists
|
||||
if (!existsSync(userClaudeDir)) {
|
||||
const fs = require('fs');
|
||||
fs.mkdirSync(userClaudeDir, { recursive: true });
|
||||
}
|
||||
|
||||
let content = '';
|
||||
if (existsSync(userClaudePath)) {
|
||||
content = readFileSync(userClaudePath, 'utf8');
|
||||
} else {
|
||||
// Create new CLAUDE.md with header
|
||||
content = '# Claude Instructions\n\n';
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
// Check if reference already exists
|
||||
if (chineseRefPattern.test(content)) {
|
||||
return { success: true, message: 'Already enabled' };
|
||||
}
|
||||
|
||||
// Add reference after the header line or at the beginning
|
||||
const headerMatch = content.match(/^# Claude Instructions\n\n?/);
|
||||
if (headerMatch) {
|
||||
const insertPosition = headerMatch[0].length;
|
||||
content = content.slice(0, insertPosition) + chineseRefLine + '\n' + content.slice(insertPosition);
|
||||
} else {
|
||||
// Add header and reference
|
||||
content = '# Claude Instructions\n\n' + chineseRefLine + '\n' + content;
|
||||
}
|
||||
} else {
|
||||
// Remove reference
|
||||
content = content.replace(chineseRefPattern, '').replace(/\n{3,}/g, '\n\n').trim();
|
||||
if (content) content += '\n';
|
||||
}
|
||||
|
||||
writeFileSync(userClaudePath, content, 'utf8');
|
||||
|
||||
// Broadcast update
|
||||
broadcastToClients({
|
||||
type: 'LANGUAGE_SETTING_CHANGED',
|
||||
data: { chineseResponse: enabled }
|
||||
});
|
||||
|
||||
return { success: true, enabled };
|
||||
} catch (error) {
|
||||
return { error: (error as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
338
ccw/src/core/routes/core-memory-routes.ts
Normal file
338
ccw/src/core/routes/core-memory-routes.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getCoreMemoryStore } from '../core-memory-store.js';
|
||||
import type { CoreMemory, KnowledgeGraph, EvolutionVersion } from '../core-memory-store.js';
|
||||
|
||||
/**
|
||||
* Route context interface
|
||||
*/
|
||||
interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: http.IncomingMessage;
|
||||
res: http.ServerResponse;
|
||||
initialPath: string;
|
||||
handlePostRequest: (req: http.IncomingMessage, res: http.ServerResponse, handler: (body: any) => Promise<any>) => void;
|
||||
broadcastToClients: (data: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Core Memory API routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleCoreMemoryRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx;
|
||||
|
||||
// API: Core Memory - Get all memories
|
||||
if (pathname === '/api/core-memory/memories' && req.method === 'GET') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const archived = url.searchParams.get('archived') === 'true';
|
||||
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const memories = store.getMemories({ archived, limit, offset });
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, memories }));
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Core Memory - Get single memory
|
||||
if (pathname.startsWith('/api/core-memory/memories/') && req.method === 'GET') {
|
||||
const memoryId = pathname.replace('/api/core-memory/memories/', '');
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const memory = store.getMemory(memoryId);
|
||||
|
||||
if (memory) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, memory }));
|
||||
} else {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Memory not found' }));
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Core Memory - Create or update memory
|
||||
if (pathname === '/api/core-memory/memories' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { content, summary, raw_output, id, archived, metadata, path: projectPath } = body;
|
||||
|
||||
if (!content) {
|
||||
return { error: 'content is required', status: 400 };
|
||||
}
|
||||
|
||||
const basePath = projectPath || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(basePath);
|
||||
const memory = store.upsertMemory({
|
||||
id,
|
||||
content,
|
||||
summary,
|
||||
raw_output,
|
||||
archived,
|
||||
metadata: metadata ? JSON.stringify(metadata) : undefined
|
||||
});
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CORE_MEMORY_UPDATED',
|
||||
payload: {
|
||||
memory,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
memory
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { error: (error as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Core Memory - Archive memory
|
||||
if (pathname.startsWith('/api/core-memory/memories/') && pathname.endsWith('/archive') && req.method === 'POST') {
|
||||
const memoryId = pathname.replace('/api/core-memory/memories/', '').replace('/archive', '');
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
store.archiveMemory(memoryId);
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CORE_MEMORY_UPDATED',
|
||||
payload: {
|
||||
memoryId,
|
||||
archived: true,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Core Memory - Delete memory
|
||||
if (pathname.startsWith('/api/core-memory/memories/') && req.method === 'DELETE') {
|
||||
const memoryId = pathname.replace('/api/core-memory/memories/', '');
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
store.deleteMemory(memoryId);
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CORE_MEMORY_UPDATED',
|
||||
payload: {
|
||||
memoryId,
|
||||
deleted: true,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
res.writeHead(204, { 'Content-Type': 'application/json' });
|
||||
res.end();
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Core Memory - Generate summary
|
||||
if (pathname.startsWith('/api/core-memory/memories/') && pathname.endsWith('/summary') && req.method === 'POST') {
|
||||
const memoryId = pathname.replace('/api/core-memory/memories/', '').replace('/summary', '');
|
||||
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { tool = 'gemini', path: projectPath } = body;
|
||||
const basePath = projectPath || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(basePath);
|
||||
const summary = await store.generateSummary(memoryId, tool);
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CORE_MEMORY_UPDATED',
|
||||
payload: {
|
||||
memoryId,
|
||||
summary,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
summary
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { error: (error as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Core Memory - Extract knowledge graph
|
||||
if (pathname.startsWith('/api/core-memory/memories/') && pathname.endsWith('/knowledge-graph') && req.method === 'GET') {
|
||||
const memoryId = pathname.replace('/api/core-memory/memories/', '').replace('/knowledge-graph', '');
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const knowledgeGraph = store.getKnowledgeGraph(memoryId);
|
||||
|
||||
// If no graph exists, extract it first
|
||||
if (knowledgeGraph.nodes.length === 0) {
|
||||
const extracted = store.extractKnowledgeGraph(memoryId);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, knowledgeGraph: extracted }));
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, knowledgeGraph }));
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Core Memory - Track evolution history
|
||||
if (pathname.startsWith('/api/core-memory/memories/') && pathname.endsWith('/evolution') && req.method === 'GET') {
|
||||
const memoryId = pathname.replace('/api/core-memory/memories/', '').replace('/evolution', '');
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const evolution = store.trackEvolution(memoryId);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, evolution }));
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Core Memory - Get aggregated graph data for graph explorer
|
||||
if (pathname === '/api/core-memory/graph' && req.method === 'GET') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const memories = store.getMemories({ archived: false });
|
||||
|
||||
// Aggregate all knowledge graphs from memories
|
||||
const aggregatedNodes: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
symbol_type?: string;
|
||||
path?: string;
|
||||
line_number?: number;
|
||||
imports?: number;
|
||||
exports?: number;
|
||||
references?: number;
|
||||
}> = [];
|
||||
|
||||
const aggregatedEdges: Array<{
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
weight: number;
|
||||
}> = [];
|
||||
|
||||
const nodeMap = new Map<string, any>();
|
||||
const edgeMap = new Map<string, any>();
|
||||
|
||||
// Collect nodes and edges from all memories
|
||||
memories.forEach((memory: CoreMemory) => {
|
||||
const graph = store.getKnowledgeGraph(memory.id);
|
||||
|
||||
// Process nodes
|
||||
graph.nodes.forEach((node: any) => {
|
||||
const nodeId = node.id || node.name;
|
||||
if (!nodeMap.has(nodeId)) {
|
||||
nodeMap.set(nodeId, {
|
||||
id: nodeId,
|
||||
name: node.name || node.label || nodeId,
|
||||
type: node.type || 'MODULE',
|
||||
symbol_type: node.symbol_type,
|
||||
path: node.path || node.file_path,
|
||||
line_number: node.line_number,
|
||||
imports: node.imports || 0,
|
||||
exports: node.exports || 0,
|
||||
references: node.references || 0
|
||||
});
|
||||
} else {
|
||||
// Aggregate counts for duplicate nodes
|
||||
const existing = nodeMap.get(nodeId);
|
||||
existing.imports = (existing.imports || 0) + (node.imports || 0);
|
||||
existing.exports = (existing.exports || 0) + (node.exports || 0);
|
||||
existing.references = (existing.references || 0) + (node.references || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Process edges
|
||||
graph.edges.forEach((edge: any) => {
|
||||
const edgeKey = `${edge.source}-${edge.target}-${edge.type || 'CALLS'}`;
|
||||
if (!edgeMap.has(edgeKey)) {
|
||||
edgeMap.set(edgeKey, {
|
||||
source: edge.source || edge.from,
|
||||
target: edge.target || edge.to,
|
||||
type: edge.type || edge.relation_type || 'CALLS',
|
||||
weight: edge.weight || 1
|
||||
});
|
||||
} else {
|
||||
// Aggregate weights for duplicate edges
|
||||
const existing = edgeMap.get(edgeKey);
|
||||
existing.weight = (existing.weight || 1) + (edge.weight || 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Convert maps to arrays
|
||||
aggregatedNodes.push(...nodeMap.values());
|
||||
aggregatedEdges.push(...edgeMap.values());
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
nodes: aggregatedNodes,
|
||||
edges: aggregatedEdges
|
||||
}));
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { resolvePath, getRecentPaths, normalizePathForDisplay } from '../utils/p
|
||||
import { handleStatusRoutes } from './routes/status-routes.js';
|
||||
import { handleCliRoutes } from './routes/cli-routes.js';
|
||||
import { handleMemoryRoutes } from './routes/memory-routes.js';
|
||||
import { handleCoreMemoryRoutes } from './routes/core-memory-routes.js';
|
||||
import { handleMcpRoutes } from './routes/mcp-routes.js';
|
||||
import { handleHooksRoutes } from './routes/hooks-routes.js';
|
||||
import { handleCodexLensRoutes } from './routes/codexlens-routes.js';
|
||||
@@ -259,8 +260,8 @@ 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/')) {
|
||||
// Claude CLAUDE.md routes (/api/memory/claude/*) and Language routes (/api/language/*)
|
||||
if (pathname.startsWith('/api/memory/claude/') || pathname.startsWith('/api/language/')) {
|
||||
if (await handleClaudeRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
@@ -269,6 +270,12 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
if (await handleMemoryRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Core Memory routes (/api/core-memory/*)
|
||||
if (pathname.startsWith('/api/core-memory/')) {
|
||||
if (await handleCoreMemoryRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
|
||||
// MCP routes (/api/mcp*, /api/codex-mcp*)
|
||||
if (pathname.startsWith('/api/mcp') || pathname.startsWith('/api/codex-mcp')) {
|
||||
if (await handleMcpRoutes(routeContext)) return;
|
||||
|
||||
Reference in New Issue
Block a user