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:
catlog22
2025-12-18 10:07:29 +08:00
parent 4329bd8e80
commit e096fc98e2
23 changed files with 3876 additions and 23 deletions

View File

@@ -10,6 +10,7 @@ import { toolCommand } from './commands/tool.js';
import { sessionCommand } from './commands/session.js';
import { cliCommand } from './commands/cli.js';
import { memoryCommand } from './commands/memory.js';
import { coreMemoryCommand } from './commands/core-memory.js';
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
@@ -193,5 +194,20 @@ export function run(argv: string[]): void {
.option('--dry-run', 'Preview without deleting')
.action((subcommand, args, options) => memoryCommand(subcommand, args, options));
// Core Memory command
program
.command('core-memory [subcommand] [args...]')
.description('Manage core memory entries for strategic context')
.option('--id <id>', 'Memory ID')
.option('--all', 'Archive all memories')
.option('--before <date>', 'Archive memories before date (YYYY-MM-DD)')
.option('--interactive', 'Interactive selection')
.option('--archived', 'List archived memories')
.option('--limit <n>', 'Number of results', '50')
.option('--json', 'Output as JSON')
.option('--force', 'Skip confirmation')
.option('--tool <tool>', 'Tool to use for summary: gemini, qwen', 'gemini')
.action((subcommand, args, options) => coreMemoryCommand(subcommand, args, options));
program.parse(argv);
}

View File

@@ -0,0 +1,201 @@
/**
* Core Memory Command - Simplified CLI for core memory management
* Four commands: list, import, export, summary
*/
import chalk from 'chalk';
import { getCoreMemoryStore } from '../core/core-memory-store.js';
import { notifyRefreshRequired } from '../tools/notifier.js';
interface CommandOptions {
id?: string;
tool?: 'gemini' | 'qwen';
}
/**
* Get project path from current working directory
*/
function getProjectPath(): string {
return process.cwd();
}
/**
* List all memories
*/
async function listAction(): Promise<void> {
try {
const store = getCoreMemoryStore(getProjectPath());
const memories = store.getMemories({ limit: 100 });
console.log(chalk.bold.cyan('\n Core Memories\n'));
if (memories.length === 0) {
console.log(chalk.yellow(' No memories found\n'));
return;
}
console.log(chalk.gray(' ─────────────────────────────────────────────────────────────────'));
for (const memory of memories) {
const date = new Date(memory.updated_at).toLocaleString();
const archived = memory.archived ? chalk.gray(' [archived]') : '';
console.log(chalk.cyan(` ${memory.id}`) + archived);
console.log(chalk.white(` ${memory.summary || memory.content.substring(0, 80)}${memory.content.length > 80 ? '...' : ''}`));
console.log(chalk.gray(` Updated: ${date}`));
console.log(chalk.gray(' ─────────────────────────────────────────────────────────────────'));
}
console.log(chalk.gray(`\n Total: ${memories.length}\n`));
} catch (error) {
console.error(chalk.red(`Error: ${(error as Error).message}`));
process.exit(1);
}
}
/**
* Import text as a new memory
*/
async function importAction(text: string): Promise<void> {
if (!text || text.trim() === '') {
console.error(chalk.red('Error: Text content is required'));
console.error(chalk.gray('Usage: ccw core-memory import "your text content here"'));
process.exit(1);
}
try {
const store = getCoreMemoryStore(getProjectPath());
const memory = store.upsertMemory({
content: text.trim()
});
console.log(chalk.green(`✓ Created memory: ${memory.id}`));
// Extract knowledge graph
store.extractKnowledgeGraph(memory.id);
// Notify dashboard
notifyRefreshRequired('memory').catch(() => { /* ignore */ });
} catch (error) {
console.error(chalk.red(`Error: ${(error as Error).message}`));
process.exit(1);
}
}
/**
* Export a memory as plain text
*/
async function exportAction(options: CommandOptions): Promise<void> {
const { id } = options;
if (!id) {
console.error(chalk.red('Error: --id is required'));
console.error(chalk.gray('Usage: ccw core-memory export --id <id>'));
process.exit(1);
}
try {
const store = getCoreMemoryStore(getProjectPath());
const memory = store.getMemory(id);
if (!memory) {
console.error(chalk.red(`Error: Memory "${id}" not found`));
process.exit(1);
}
// Output plain text content
console.log(memory.content);
} catch (error) {
console.error(chalk.red(`Error: ${(error as Error).message}`));
process.exit(1);
}
}
/**
* Generate summary for a memory
*/
async function summaryAction(options: CommandOptions): Promise<void> {
const { id, tool = 'gemini' } = options;
if (!id) {
console.error(chalk.red('Error: --id is required'));
console.error(chalk.gray('Usage: ccw core-memory summary --id <id> [--tool gemini|qwen]'));
process.exit(1);
}
try {
const store = getCoreMemoryStore(getProjectPath());
const memory = store.getMemory(id);
if (!memory) {
console.error(chalk.red(`Error: Memory "${id}" not found`));
process.exit(1);
}
console.log(chalk.cyan(`Generating summary using ${tool}...`));
const summary = await store.generateSummary(id, tool);
console.log(chalk.green('\n✓ Summary generated:\n'));
console.log(chalk.white(` ${summary}\n`));
// Notify dashboard
notifyRefreshRequired('memory').catch(() => { /* ignore */ });
} catch (error) {
console.error(chalk.red(`Error: ${(error as Error).message}`));
process.exit(1);
}
}
/**
* Core Memory command entry point
*/
export async function coreMemoryCommand(
subcommand: string,
args: string | string[],
options: CommandOptions
): Promise<void> {
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
const textArg = argsArray.join(' ');
switch (subcommand) {
case 'list':
await listAction();
break;
case 'import':
await importAction(textArg);
break;
case 'export':
await exportAction(options);
break;
case 'summary':
await summaryAction(options);
break;
default:
console.log(chalk.bold.cyan('\n CCW Core Memory\n'));
console.log(' Manage core memory entries.\n');
console.log(' Commands:');
console.log(chalk.white(' list ') + chalk.gray('List all memories'));
console.log(chalk.white(' import "<text>" ') + chalk.gray('Import text as new memory'));
console.log(chalk.white(' export --id <id> ') + chalk.gray('Export memory as plain text'));
console.log(chalk.white(' summary --id <id> ') + chalk.gray('Generate AI summary'));
console.log();
console.log(' Options:');
console.log(chalk.gray(' --id <id> Memory ID (for export/summary)'));
console.log(chalk.gray(' --tool gemini|qwen AI tool for summary (default: gemini)'));
console.log();
console.log(' Examples:');
console.log(chalk.gray(' ccw core-memory list'));
console.log(chalk.gray(' ccw core-memory import "This is important context about the auth module"'));
console.log(chalk.gray(' ccw core-memory export --id CMEM-20251217-143022'));
console.log(chalk.gray(' ccw core-memory summary --id CMEM-20251217-143022'));
console.log();
}
}

View 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;

View File

@@ -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'
];

View File

@@ -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;
}

View 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;
}

View File

@@ -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;

View File

@@ -17,7 +17,7 @@ const SERVER_NAME = 'ccw-tools';
const SERVER_VERSION = '6.1.4';
// Default enabled tools (core set)
const DEFAULT_TOOLS: string[] = ['write_file', 'edit_file', 'read_file', 'smart_search'];
const DEFAULT_TOOLS: string[] = ['write_file', 'edit_file', 'read_file', 'smart_search', 'core_memory'];
/**
* Get list of enabled tools from environment or defaults

View File

@@ -3552,6 +3552,29 @@
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.5);
}
/* Language Setting Status Badge */
.cli-setting-status {
margin-left: 0.75rem;
padding: 0.25rem 0.625rem;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
border-radius: 0.25rem;
display: inline-flex;
align-items: center;
}
.cli-setting-status.enabled {
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
}
.cli-setting-status.disabled {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
/* Ghost button variant for destructive actions */
.btn-ghost.text-destructive {
color: hsl(var(--destructive));

View File

@@ -1285,6 +1285,30 @@
color: hsl(var(--muted-foreground));
}
/* Data Source Selector */
.data-source-select {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
font-weight: 500;
color: hsl(var(--foreground));
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
}
.data-source-select:hover {
background: hsl(var(--hover));
border-color: hsl(var(--primary) / 0.3);
}
.data-source-select:focus {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1);
}
.btn-icon {
display: flex;
align-items: center;

View File

@@ -0,0 +1,477 @@
/* ============================================
Core Memory Styles
============================================ */
.core-memory-container {
padding: 1.5rem;
}
.core-memory-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.header-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.memory-stats {
display: flex;
gap: 1.5rem;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-label {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
}
/* Memories Grid */
.memories-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.25rem;
}
@media (max-width: 768px) {
.memories-grid {
grid-template-columns: 1fr;
}
}
/* Memory Card */
.memory-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.25rem;
transition: all 0.2s ease;
}
.memory-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.memory-card.archived {
opacity: 0.7;
border-style: dashed;
}
.memory-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.memory-id {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
flex-wrap: wrap;
}
.memory-id i {
width: 16px;
height: 16px;
}
.memory-actions {
display: flex;
gap: 0.5rem;
}
.icon-btn {
background: none;
border: none;
padding: 0.25rem;
cursor: pointer;
color: var(--text-secondary);
transition: color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.icon-btn:hover {
color: var(--text-primary);
}
.icon-btn.danger:hover {
color: var(--danger-color);
}
.icon-btn i {
width: 18px;
height: 18px;
}
/* Memory Content */
.memory-content {
margin-bottom: 1rem;
}
.memory-summary {
font-size: 0.9375rem;
line-height: 1.6;
color: var(--text-primary);
}
.memory-preview {
font-size: 0.875rem;
line-height: 1.5;
color: var(--text-secondary);
}
/* Memory Tags */
.memory-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tag {
padding: 0.25rem 0.75rem;
background: var(--accent-bg);
color: var(--accent-color);
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
/* Memory Footer */
.memory-footer {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.memory-meta {
display: flex;
gap: 1rem;
font-size: 0.8125rem;
color: var(--text-secondary);
flex-wrap: wrap;
}
.memory-meta span {
display: flex;
align-items: center;
gap: 0.375rem;
}
.memory-meta i {
width: 14px;
height: 14px;
}
.memory-features {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.feature-btn {
padding: 0.375rem 0.75rem;
background: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.8125rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.375rem;
color: var(--text-primary);
}
.feature-btn:hover {
background: var(--hover-bg);
border-color: var(--accent-color);
}
.feature-btn i {
width: 14px;
height: 14px;
}
/* Badges */
.badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-archived {
background: var(--warning-bg, #fef3c7);
color: var(--warning-color, #92400e);
}
.badge-priority-high {
background: var(--danger-bg, #fee2e2);
color: var(--danger-color, #991b1b);
}
.badge-priority-low {
background: var(--info-bg, #dbeafe);
color: var(--info-color, #1e3a8a);
}
.badge-current {
background: var(--success-bg, #d1fae5);
color: var(--success-color, #065f46);
}
/* Modal Styles */
.memory-modal {
max-width: 700px;
width: 100%;
}
.memory-detail-modal {
max-width: 900px;
width: 100%;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.form-group textarea,
.form-group input[type="text"] {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--input-bg);
color: var(--text-primary);
font-family: inherit;
font-size: 0.9375rem;
resize: vertical;
}
.form-group textarea:focus,
.form-group input[type="text"]:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px var(--accent-shadow, rgba(99, 102, 241, 0.1));
}
/* Memory Detail Content */
.memory-detail-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.detail-section h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.detail-text {
font-size: 0.9375rem;
line-height: 1.6;
color: var(--text-primary);
white-space: pre-wrap;
}
.detail-code {
background: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 1rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Knowledge Graph Styles */
.knowledge-graph {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.graph-section h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.entities-list,
.relationships-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.entity-item {
padding: 0.75rem;
background: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.entity-name {
font-weight: 500;
color: var(--text-primary);
}
.entity-type {
padding: 0.25rem 0.5rem;
background: var(--accent-bg);
color: var(--accent-color);
border-radius: 4px;
font-size: 0.75rem;
}
.relationship-item {
padding: 0.75rem;
background: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
display: flex;
align-items: center;
gap: 0.75rem;
}
.rel-source,
.rel-target {
flex: 1;
font-weight: 500;
color: var(--text-primary);
}
.rel-type {
padding: 0.25rem 0.5rem;
background: var(--info-bg, #dbeafe);
color: var(--info-color, #1e3a8a);
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
}
/* Evolution Timeline */
.evolution-timeline {
display: flex;
flex-direction: column;
gap: 1rem;
}
.evolution-version {
padding: 1rem;
background: var(--secondary-bg);
border: 1px solid var(--border-color);
border-left: 3px solid var(--accent-color);
border-radius: 6px;
}
.version-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.version-number {
font-weight: 600;
color: var(--accent-color);
}
.version-date {
font-size: 0.8125rem;
color: var(--text-secondary);
}
.version-reason {
font-size: 0.875rem;
color: var(--text-primary);
margin-top: 0.5rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--text-secondary);
}
.empty-state i {
width: 48px;
height: 48px;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
font-size: 0.9375rem;
}
.empty-text {
color: var(--text-secondary);
font-style: italic;
}
/* Dark Mode Adjustments */
[data-theme="dark"] .memory-card {
background: #1e293b;
border-color: #334155;
}
[data-theme="dark"] .memory-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
[data-theme="dark"] .detail-code {
background: #0f172a;
border-color: #334155;
}
[data-theme="dark"] .entity-item,
[data-theme="dark"] .relationship-item,
[data-theme="dark"] .evolution-version {
background: #1e293b;
border-color: #334155;
}

View File

@@ -0,0 +1,273 @@
// ==========================================
// INDEX MANAGER COMPONENT
// ==========================================
// Manages CodexLens code indexes (vector and normal)
// State
let indexData = null;
let indexLoading = false;
/**
* Initialize index manager
*/
async function initIndexManager() {
await loadIndexStats();
}
/**
* Load index statistics from API
*/
async function loadIndexStats() {
if (indexLoading) return;
indexLoading = true;
try {
const res = await fetch('/api/codexlens/indexes');
if (!res.ok) throw new Error('Failed to load index stats');
indexData = await res.json();
renderIndexCard();
} catch (err) {
console.error('Failed to load index stats:', err);
renderIndexCardError(err.message);
} finally {
indexLoading = false;
}
}
/**
* Render index card in the dashboard
*/
function renderIndexCard() {
const container = document.getElementById('indexCard');
if (!container || !indexData) return;
const { indexDir, indexes, summary } = indexData;
// Format relative time
const formatTimeAgo = (isoString) => {
if (!isoString) return t('common.never') || 'Never';
const date = new Date(isoString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return t('common.justNow') || 'Just now';
if (diffMins < 60) return diffMins + 'm ' + (t('common.ago') || 'ago');
if (diffHours < 24) return diffHours + 'h ' + (t('common.ago') || 'ago');
if (diffDays < 30) return diffDays + 'd ' + (t('common.ago') || 'ago');
return date.toLocaleDateString();
};
// Build index rows
let indexRows = '';
if (indexes && indexes.length > 0) {
indexes.forEach(function(idx) {
const vectorBadge = idx.hasVectorIndex
? '<span class="text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded">' + (t('index.vector') || 'Vector') + '</span>'
: '';
const normalBadge = idx.hasNormalIndex
? '<span class="text-xs px-1.5 py-0.5 bg-muted text-muted-foreground rounded">' + (t('index.fts') || 'FTS') + '</span>'
: '';
indexRows += '\
<tr class="border-t border-border hover:bg-muted/30 transition-colors">\
<td class="py-2 px-2 text-foreground">\
<div class="flex items-center gap-2">\
<span class="font-mono text-xs truncate max-w-[250px]" title="' + escapeHtml(idx.id) + '">' + escapeHtml(idx.id) + '</span>\
</div>\
</td>\
<td class="py-2 px-2 text-right text-muted-foreground">' + idx.sizeFormatted + '</td>\
<td class="py-2 px-2 text-center">\
<div class="flex items-center justify-center gap-1">' + vectorBadge + normalBadge + '</div>\
</td>\
<td class="py-2 px-2 text-right text-muted-foreground">' + formatTimeAgo(idx.lastModified) + '</td>\
<td class="py-2 px-1 text-center">\
<button onclick="cleanIndexProject(\'' + escapeHtml(idx.id) + '\')" \
class="text-destructive/70 hover:text-destructive p-1 rounded hover:bg-destructive/10 transition-colors" \
title="' + (t('index.cleanProject') || 'Clean Index') + '">\
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>\
</button>\
</td>\
</tr>\
';
});
} else {
indexRows = '\
<tr>\
<td colspan="5" class="py-4 text-center text-muted-foreground text-sm">' + (t('index.noIndexes') || 'No indexes yet') + '</td>\
</tr>\
';
}
container.innerHTML = '\
<div class="bg-card border border-border rounded-lg overflow-hidden">\
<div class="bg-muted/30 border-b border-border px-4 py-3 flex items-center justify-between">\
<div class="flex items-center gap-2">\
<i data-lucide="database" class="w-4 h-4 text-primary"></i>\
<span class="font-medium text-foreground">' + (t('index.manager') || 'Index Manager') + '</span>\
<span class="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">' + (summary?.totalSizeFormatted || '0 B') + '</span>\
</div>\
<div class="flex items-center gap-2">\
<button onclick="loadIndexStats()" class="text-xs px-2 py-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors" title="' + (t('common.refresh') || 'Refresh') + '">\
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>\
</button>\
<button onclick="showCodexLensConfigModal()" class="text-xs px-2 py-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors" title="' + (t('common.settings') || 'Settings') + '">\
<i data-lucide="settings" class="w-3.5 h-3.5"></i>\
</button>\
</div>\
</div>\
<div class="p-4">\
<div class="flex items-center gap-2 mb-3 text-xs text-muted-foreground">\
<i data-lucide="folder" class="w-3.5 h-3.5"></i>\
<span class="font-mono truncate" title="' + escapeHtml(indexDir || '') + '">' + escapeHtml(indexDir || t('index.notConfigured') || 'Not configured') + '</span>\
</div>\
<div class="grid grid-cols-4 gap-3 mb-4">\
<div class="bg-muted/30 rounded-lg p-3 text-center">\
<div class="text-lg font-semibold text-foreground">' + (summary?.totalProjects || 0) + '</div>\
<div class="text-xs text-muted-foreground">' + (t('index.projects') || 'Projects') + '</div>\
</div>\
<div class="bg-muted/30 rounded-lg p-3 text-center">\
<div class="text-lg font-semibold text-foreground">' + (summary?.totalSizeFormatted || '0 B') + '</div>\
<div class="text-xs text-muted-foreground">' + (t('index.totalSize') || 'Total Size') + '</div>\
</div>\
<div class="bg-muted/30 rounded-lg p-3 text-center">\
<div class="text-lg font-semibold text-foreground">' + (summary?.vectorIndexCount || 0) + '</div>\
<div class="text-xs text-muted-foreground">' + (t('index.vectorIndexes') || 'Vector') + '</div>\
</div>\
<div class="bg-muted/30 rounded-lg p-3 text-center">\
<div class="text-lg font-semibold text-foreground">' + (summary?.normalIndexCount || 0) + '</div>\
<div class="text-xs text-muted-foreground">' + (t('index.ftsIndexes') || 'FTS') + '</div>\
</div>\
</div>\
<div class="border border-border rounded-lg overflow-hidden">\
<table class="w-full text-sm">\
<thead class="bg-muted/50">\
<tr class="text-xs text-muted-foreground">\
<th class="py-2 px-2 text-left font-medium">' + (t('index.projectId') || 'Project ID') + '</th>\
<th class="py-2 px-2 text-right font-medium">' + (t('index.size') || 'Size') + '</th>\
<th class="py-2 px-2 text-center font-medium">' + (t('index.type') || 'Type') + '</th>\
<th class="py-2 px-2 text-right font-medium">' + (t('index.lastModified') || 'Modified') + '</th>\
<th class="py-2 px-1 w-8"></th>\
</tr>\
</thead>\
<tbody>\
' + indexRows + '\
</tbody>\
</table>\
</div>\
<div class="mt-4 flex justify-between items-center gap-2">\
<button onclick="initCodexLensIndex()" \
class="text-xs px-3 py-1.5 bg-primary/10 text-primary hover:bg-primary/20 rounded transition-colors flex items-center gap-1.5">\
<i data-lucide="database" class="w-3.5 h-3.5"></i>\
' + (t('index.initCurrent') || 'Init Current Project') + '\
</button>\
<button onclick="cleanAllIndexesConfirm()" \
class="text-xs px-3 py-1.5 bg-destructive/10 text-destructive hover:bg-destructive/20 rounded transition-colors flex items-center gap-1.5">\
<i data-lucide="trash" class="w-3.5 h-3.5"></i>\
' + (t('index.cleanAll') || 'Clean All') + '\
</button>\
</div>\
</div>\
</div>\
';
// Reinitialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
/**
* Render error state for index card
*/
function renderIndexCardError(errorMessage) {
const container = document.getElementById('indexCard');
if (!container) return;
container.innerHTML = '\
<div class="bg-card border border-border rounded-lg overflow-hidden">\
<div class="bg-muted/30 border-b border-border px-4 py-3 flex items-center gap-2">\
<i data-lucide="database" class="w-4 h-4 text-primary"></i>\
<span class="font-medium text-foreground">' + (t('index.manager') || 'Index Manager') + '</span>\
</div>\
<div class="p-4 text-center">\
<div class="text-destructive mb-2">\
<i data-lucide="alert-circle" class="w-8 h-8 mx-auto"></i>\
</div>\
<p class="text-sm text-muted-foreground mb-3">' + escapeHtml(errorMessage) + '</p>\
<button onclick="loadIndexStats()" \
class="text-xs px-3 py-1.5 bg-primary text-primary-foreground hover:bg-primary/90 rounded transition-colors">\
' + (t('common.retry') || 'Retry') + '\
</button>\
</div>\
</div>\
';
// Reinitialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
/**
* Clean a specific project's index
*/
async function cleanIndexProject(projectId) {
if (!confirm((t('index.cleanProjectConfirm') || 'Clean index for') + ' ' + projectId + '?')) {
return;
}
try {
showRefreshToast(t('index.cleaning') || 'Cleaning index...', 'info');
// The project ID is the directory name in the index folder
// We need to construct the full path or use a clean API
const response = await fetch('/api/codexlens/clean', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: projectId })
});
const result = await response.json();
if (result.success) {
showRefreshToast(t('index.cleanSuccess') || 'Index cleaned successfully', 'success');
await loadIndexStats();
} else {
showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error');
}
} catch (err) {
showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
}
}
/**
* Confirm and clean all indexes
*/
async function cleanAllIndexesConfirm() {
if (!confirm(t('index.cleanAllConfirm') || 'Are you sure you want to clean ALL indexes? This cannot be undone.')) {
return;
}
try {
showRefreshToast(t('index.cleaning') || 'Cleaning indexes...', 'info');
const response = await fetch('/api/codexlens/clean', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ all: true })
});
const result = await response.json();
if (result.success) {
showRefreshToast(t('index.cleanAllSuccess') || 'All indexes cleaned', 'success');
await loadIndexStats();
} else {
showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error');
}
} catch (err) {
showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
}
}

View File

@@ -137,6 +137,8 @@ function initNavigation() {
renderGraphExplorer();
} else if (currentView === 'help') {
renderHelpView();
} else if (currentView === 'core-memory') {
renderCoreMemoryView();
}
});
});
@@ -175,6 +177,8 @@ function updateContentTitle() {
titleEl.textContent = t('title.graphExplorer');
} else if (currentView === 'help') {
titleEl.textContent = t('title.helpGuide');
} else if (currentView === 'core-memory') {
titleEl.textContent = t('title.coreMemory');
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');

View File

@@ -29,6 +29,7 @@ const i18n = {
'nav.history': 'History',
'nav.memory': 'Memory',
'nav.contextMemory': 'Context',
'nav.coreMemory': 'Core Memory',
'nav.promptHistory': 'Prompts',
// Sidebar - Sessions section
@@ -287,6 +288,30 @@ const i18n = {
'codexlens.indexSuccess': 'Index created successfully',
'codexlens.indexFailed': 'Indexing failed',
// Index Manager
'index.manager': 'Index Manager',
'index.projects': 'Projects',
'index.totalSize': 'Total Size',
'index.vectorIndexes': 'Vector',
'index.ftsIndexes': 'FTS',
'index.projectId': 'Project ID',
'index.size': 'Size',
'index.type': 'Type',
'index.lastModified': 'Modified',
'index.vector': 'Vector',
'index.fts': 'FTS',
'index.noIndexes': 'No indexes yet',
'index.notConfigured': 'Not configured',
'index.initCurrent': 'Init Current Project',
'index.cleanAll': 'Clean All',
'index.cleanProject': 'Clean Index',
'index.cleanProjectConfirm': 'Clean index for',
'index.cleaning': 'Cleaning index...',
'index.cleanSuccess': 'Index cleaned successfully',
'index.cleanFailed': 'Clean failed',
'index.cleanAllConfirm': 'Are you sure you want to clean ALL indexes? This cannot be undone.',
'index.cleanAllSuccess': 'All indexes cleaned',
// Semantic Search Configuration
'semantic.settings': 'Semantic Search Settings',
'semantic.testSearch': 'Test Semantic Search',
@@ -294,7 +319,19 @@ const i18n = {
'semantic.runSearch': 'Run Semantic Search',
'semantic.close': 'Close',
'cli.settings': 'CLI Execution Settings',
'cli.settings': 'Settings',
// Language Settings
'lang.settings': 'Response Language',
'lang.settingsDesc': 'Configure Claude response language preference',
'lang.chinese': 'Chinese Response',
'lang.chineseDesc': 'Enable Chinese response guidelines in global CLAUDE.md',
'lang.enabled': 'Enabled',
'lang.disabled': 'Disabled',
'lang.enableSuccess': 'Chinese response enabled',
'lang.disableSuccess': 'Chinese response disabled',
'lang.enableFailed': 'Failed to enable Chinese response',
'lang.disableFailed': 'Failed to disable Chinese response',
'cli.promptFormat': 'Prompt Format',
'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation',
'cli.storageBackend': 'Storage Backend',
@@ -1060,15 +1097,17 @@ const i18n = {
'graph.searchProcessDesc': 'Visualize how search queries flow through the system',
'graph.searchProcessTitle': 'Search Pipeline',
'graph.resultsFound': 'results found',
'graph.coreMemory': 'Core Memory',
'graph.dataSourceSwitched': 'Data source switched',
'graph.type': 'Type',
'graph.line': 'Line',
'graph.path': 'Path',
'graph.depth': 'Depth',
'graph.exports': 'Exports',
'graph.imports': 'Imports',
'graph.references': 'References',
'graph.symbolType': 'Symbol Type',
'graph.path': 'Path',
'graph.line': 'Line',
'graph.imports': 'imports',
'graph.exports': 'exports',
'graph.references': 'references',
'graph.affectedSymbols': 'Affected Symbols',
'graph.depth': 'Depth',
// CLI Sync (used in claude-manager.js)
'claude.cliSync': 'CLI Auto-Sync',
@@ -1131,6 +1170,56 @@ const i18n = {
'common.saveFailed': 'Failed to save',
'common.unknownError': 'Unknown error',
'common.exception': 'Exception',
// Core Memory
'title.coreMemory': 'Core Memory',
'coreMemory.createNew': 'Create Memory',
'coreMemory.showArchived': 'Show Archived',
'coreMemory.showActive': 'Show Active',
'coreMemory.totalMemories': 'Total Memories',
'coreMemory.noMemories': 'No memories found',
'coreMemory.noArchivedMemories': 'No archived memories',
'coreMemory.content': 'Content',
'coreMemory.contentPlaceholder': 'Enter strategic context, insights, or important information...',
'coreMemory.contentRequired': 'Content is required',
'coreMemory.summary': 'Summary',
'coreMemory.summaryPlaceholder': 'Optional: Brief summary of this memory...',
'coreMemory.metadata': 'Metadata',
'coreMemory.invalidMetadata': 'Invalid JSON metadata',
'coreMemory.rawOutput': 'Raw Output',
'coreMemory.created': 'Memory created successfully',
'coreMemory.updated': 'Memory updated successfully',
'coreMemory.archived': 'Memory archived successfully',
'coreMemory.unarchived': 'Memory unarchived successfully',
'coreMemory.deleted': 'Memory deleted successfully',
'coreMemory.confirmArchive': 'Archive this memory?',
'coreMemory.confirmDelete': 'Permanently delete this memory?',
'coreMemory.fetchError': 'Failed to fetch memories',
'coreMemory.saveError': 'Failed to save memory',
'coreMemory.archiveError': 'Failed to archive memory',
'coreMemory.unarchiveError': 'Failed to unarchive memory',
'coreMemory.deleteError': 'Failed to delete memory',
'coreMemory.edit': 'Edit Memory',
'coreMemory.unarchive': 'Unarchive',
'coreMemory.generateSummary': 'Generate Summary',
'coreMemory.generatingSummary': 'Generating summary...',
'coreMemory.summaryGenerated': 'Summary generated successfully',
'coreMemory.summaryError': 'Failed to generate summary',
'coreMemory.knowledgeGraph': 'Knowledge Graph',
'coreMemory.graph': 'Graph',
'coreMemory.entities': 'Entities',
'coreMemory.noEntities': 'No entities found',
'coreMemory.relationships': 'Relationships',
'coreMemory.noRelationships': 'No relationships found',
'coreMemory.graphError': 'Failed to load knowledge graph',
'coreMemory.evolution': 'Evolution',
'coreMemory.evolutionHistory': 'Evolution History',
'coreMemory.noHistory': 'No evolution history',
'coreMemory.noReason': 'No reason provided',
'coreMemory.current': 'Current',
'coreMemory.evolutionError': 'Failed to load evolution history',
'coreMemory.created': 'Created',
'coreMemory.updated': 'Updated',
},
zh: {
@@ -1154,6 +1243,7 @@ const i18n = {
'nav.history': '历史',
'nav.memory': '记忆',
'nav.contextMemory': '活动',
'nav.coreMemory': '核心记忆',
'nav.promptHistory': '洞察',
// Sidebar - Sessions section
@@ -1412,6 +1502,30 @@ const i18n = {
'codexlens.indexSuccess': '索引创建成功',
'codexlens.indexFailed': '索引失败',
// 索引管理器
'index.manager': '索引管理器',
'index.projects': '项目数',
'index.totalSize': '总大小',
'index.vectorIndexes': '向量',
'index.ftsIndexes': '全文',
'index.projectId': '项目 ID',
'index.size': '大小',
'index.type': '类型',
'index.lastModified': '修改时间',
'index.vector': '向量',
'index.fts': '全文',
'index.noIndexes': '暂无索引',
'index.notConfigured': '未配置',
'index.initCurrent': '索引当前项目',
'index.cleanAll': '清理全部',
'index.cleanProject': '清理索引',
'index.cleanProjectConfirm': '清理索引:',
'index.cleaning': '清理索引中...',
'index.cleanSuccess': '索引清理成功',
'index.cleanFailed': '清理失败',
'index.cleanAllConfirm': '确定要清理所有索引吗?此操作无法撤销。',
'index.cleanAllSuccess': '所有索引已清理',
// Semantic Search 配置
'semantic.settings': '语义搜索设置',
'semantic.testSearch': '测试语义搜索',
@@ -1419,7 +1533,19 @@ const i18n = {
'semantic.runSearch': '运行语义搜索',
'semantic.close': '关闭',
'cli.settings': 'CLI 调用设置',
'cli.settings': '设置',
// 语言设置
'lang.settings': '回复语言',
'lang.settingsDesc': '配置 Claude 回复语言偏好',
'lang.chinese': '中文回复',
'lang.chineseDesc': '在全局 CLAUDE.md 中启用中文回复准则',
'lang.enabled': '已启用',
'lang.disabled': '已禁用',
'lang.enableSuccess': '中文回复已启用',
'lang.disableSuccess': '中文回复已禁用',
'lang.enableFailed': '启用中文回复失败',
'lang.disableFailed': '禁用中文回复失败',
'cli.promptFormat': '提示词格式',
'cli.promptFormatDesc': '多轮对话拼接格式',
'cli.storageBackend': '存储后端',
@@ -2163,6 +2289,8 @@ const i18n = {
'graph.searchProcessDesc': '可视化搜索查询在系统中的流转过程',
'graph.searchProcessTitle': '搜索管道',
'graph.resultsFound': '个结果',
'graph.coreMemory': '核心记忆',
'graph.dataSourceSwitched': '数据源已切换',
'graph.type': '类型',
'graph.line': '行号',
'graph.path': '路径',
@@ -2265,6 +2393,56 @@ const i18n = {
'common.saveFailed': '保存失败',
'common.unknownError': '未知错误',
'common.exception': '异常',
// Core Memory
'title.coreMemory': '核心记忆',
'coreMemory.createNew': '创建记忆',
'coreMemory.showArchived': '显示已归档',
'coreMemory.showActive': '显示活动',
'coreMemory.totalMemories': '总记忆数',
'coreMemory.noMemories': '未找到记忆',
'coreMemory.noArchivedMemories': '没有已归档的记忆',
'coreMemory.content': '内容',
'coreMemory.contentPlaceholder': '输入战略性上下文、见解或重要信息...',
'coreMemory.contentRequired': '内容为必填项',
'coreMemory.summary': '摘要',
'coreMemory.summaryPlaceholder': '可选:此记忆的简要摘要...',
'coreMemory.metadata': '元数据',
'coreMemory.invalidMetadata': '无效的 JSON 元数据',
'coreMemory.rawOutput': '原始输出',
'coreMemory.created': '记忆创建成功',
'coreMemory.updated': '记忆更新成功',
'coreMemory.archived': '记忆已归档',
'coreMemory.unarchived': '记忆已取消归档',
'coreMemory.deleted': '记忆已删除',
'coreMemory.confirmArchive': '归档此记忆?',
'coreMemory.confirmDelete': '永久删除此记忆?',
'coreMemory.fetchError': '获取记忆失败',
'coreMemory.saveError': '保存记忆失败',
'coreMemory.archiveError': '归档记忆失败',
'coreMemory.unarchiveError': '取消归档失败',
'coreMemory.deleteError': '删除记忆失败',
'coreMemory.edit': '编辑记忆',
'coreMemory.unarchive': '取消归档',
'coreMemory.generateSummary': '生成摘要',
'coreMemory.generatingSummary': '正在生成摘要...',
'coreMemory.summaryGenerated': '摘要生成成功',
'coreMemory.summaryError': '生成摘要失败',
'coreMemory.knowledgeGraph': '知识图谱',
'coreMemory.graph': '图谱',
'coreMemory.entities': '实体',
'coreMemory.noEntities': '未找到实体',
'coreMemory.relationships': '关系',
'coreMemory.noRelationships': '未找到关系',
'coreMemory.graphError': '加载知识图谱失败',
'coreMemory.evolution': '演化',
'coreMemory.evolutionHistory': '演化历史',
'coreMemory.noHistory': '无演化历史',
'coreMemory.noReason': '未提供原因',
'coreMemory.current': '当前',
'coreMemory.evolutionError': '加载演化历史失败',
'coreMemory.created': '创建时间',
'coreMemory.updated': '更新时间',
}
};

View File

@@ -297,10 +297,6 @@ async function renderCliManager() {
if (statsGrid) statsGrid.style.display = 'none';
if (searchInput) searchInput.parentElement.style.display = 'none';
// Show storage card (only visible in CLI Manager view)
var storageCard = document.getElementById('storageCard');
if (storageCard) storageCard.style.display = '';
// Load data (including CodexLens status for tools section)
await Promise.all([
loadCliToolStatus(),
@@ -314,13 +310,17 @@ async function renderCliManager() {
'<div class="cli-section" id="tools-section"></div>' +
'<div class="cli-section" id="ccw-section"></div>' +
'</div>' +
'<div class="cli-section" id="language-settings-section" style="margin-top: 1.5rem;"></div>' +
'<div class="cli-settings-section" id="cli-settings-section" style="margin-top: 1.5rem;"></div>' +
'<div class="cli-section" id="ccw-endpoint-tools-section" style="margin-top: 1.5rem;"></div>' +
'</div>';
'</div>' +
'<section id="storageCard" class="mb-6"></section>' +
'<section id="indexCard" class="mb-6"></section>';
// Render sub-panels
renderToolsSection();
renderCcwSection();
renderLanguageSettingsSection();
renderCliSettingsSection();
renderCcwEndpointToolsSection();
@@ -329,6 +329,11 @@ async function renderCliManager() {
initStorageManager();
}
// Initialize index manager card
if (typeof initIndexManager === 'function') {
initIndexManager();
}
// Initialize Lucide icons
if (window.lucide) lucide.createIcons();
}
@@ -504,6 +509,94 @@ function renderCcwSection() {
if (window.lucide) lucide.createIcons();
}
// ========== Language Settings State ==========
var chineseResponseEnabled = false;
var chineseResponseLoading = false;
// ========== Language Settings Section ==========
async function loadLanguageSettings() {
try {
var response = await fetch('/api/language/chinese-response');
if (!response.ok) throw new Error('Failed to load language settings');
var data = await response.json();
chineseResponseEnabled = data.enabled || false;
return data;
} catch (err) {
console.error('Failed to load language settings:', err);
chineseResponseEnabled = false;
return { enabled: false, guidelinesExists: false };
}
}
async function toggleChineseResponse(enabled) {
if (chineseResponseLoading) return;
chineseResponseLoading = true;
try {
var response = await fetch('/api/language/chinese-response', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
});
if (!response.ok) {
var errData = await response.json();
throw new Error(errData.error || 'Failed to update setting');
}
var data = await response.json();
chineseResponseEnabled = data.enabled;
// Update UI
renderLanguageSettingsSection();
// Show toast
showRefreshToast(enabled ? t('lang.enableSuccess') : t('lang.disableSuccess'), 'success');
} catch (err) {
console.error('Failed to toggle Chinese response:', err);
showRefreshToast(enabled ? t('lang.enableFailed') : t('lang.disableFailed'), 'error');
} finally {
chineseResponseLoading = false;
}
}
async function renderLanguageSettingsSection() {
var container = document.getElementById('language-settings-section');
if (!container) return;
// Load current state if not loaded
if (!chineseResponseEnabled && !chineseResponseLoading) {
await loadLanguageSettings();
}
var settingsHtml = '<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="languages" class="w-4 h-4"></i> ' + t('lang.settings') + '</h3>' +
'</div>' +
'</div>' +
'<div class="cli-settings-grid" style="grid-template-columns: 1fr;">' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="message-square-text" class="w-3 h-3"></i>' +
t('lang.chinese') +
'</label>' +
'<div class="cli-setting-control">' +
'<label class="cli-toggle">' +
'<input type="checkbox"' + (chineseResponseEnabled ? ' checked' : '') + ' onchange="toggleChineseResponse(this.checked)"' + (chineseResponseLoading ? ' disabled' : '') + '>' +
'<span class="cli-toggle-slider"></span>' +
'</label>' +
'<span class="cli-setting-status ' + (chineseResponseEnabled ? 'enabled' : 'disabled') + '">' +
(chineseResponseEnabled ? t('lang.enabled') : t('lang.disabled')) +
'</span>' +
'</div>' +
'<p class="cli-setting-desc">' + t('lang.chineseDesc') + '</p>' +
'</div>' +
'</div>';
container.innerHTML = settingsHtml;
if (window.lucide) lucide.createIcons();
}
// ========== CLI Settings Section (Full Width) ==========
function renderCliSettingsSection() {
var container = document.getElementById('cli-settings-section');

View File

@@ -0,0 +1,293 @@
// Knowledge Graph and Evolution visualization functions for Core Memory
async function viewKnowledgeGraph(memoryId) {
try {
const response = await fetch(`/api/core-memory/memories/${memoryId}/knowledge-graph?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const graph = await response.json();
const modal = document.getElementById('memoryDetailModal');
document.getElementById('memoryDetailTitle').textContent = `${t('coreMemory.knowledgeGraph')} - ${memoryId}`;
const body = document.getElementById('memoryDetailBody');
body.innerHTML = `
<div class="knowledge-graph">
<div id="knowledgeGraphContainer" class="knowledge-graph-container"></div>
</div>
`;
modal.style.display = 'flex';
lucide.createIcons();
// Render D3 graph after modal is visible
setTimeout(() => {
renderKnowledgeGraphD3(graph);
}, 100);
} catch (error) {
console.error('Failed to fetch knowledge graph:', error);
showNotification(t('coreMemory.graphError'), 'error');
}
}
function renderKnowledgeGraphD3(graph) {
// Check if D3 is available
if (typeof d3 === 'undefined') {
const container = document.getElementById('knowledgeGraphContainer');
if (container) {
container.innerHTML = `
<div class="graph-error">
<i data-lucide="alert-triangle"></i>
<p>D3.js not loaded</p>
</div>
`;
lucide.createIcons();
}
return;
}
if (!graph || !graph.entities || graph.entities.length === 0) {
const container = document.getElementById('knowledgeGraphContainer');
if (container) {
container.innerHTML = `
<div class="graph-empty-state">
<i data-lucide="network"></i>
<p>${t('coreMemory.noEntities')}</p>
</div>
`;
lucide.createIcons();
}
return;
}
const container = document.getElementById('knowledgeGraphContainer');
if (!container) return;
const width = container.clientWidth || 800;
const height = 400;
// Clear existing
container.innerHTML = '';
// Transform data to D3 format
const nodes = graph.entities.map(entity => ({
id: entity.name,
name: entity.name,
type: entity.type || 'entity',
displayName: entity.name.length > 25 ? entity.name.substring(0, 22) + '...' : entity.name
}));
const nodeIds = new Set(nodes.map(n => n.id));
const edges = (graph.relationships || []).filter(rel =>
nodeIds.has(rel.source) && nodeIds.has(rel.target)
).map(rel => ({
source: rel.source,
target: rel.target,
type: rel.type || 'related'
}));
// Create SVG with zoom support
graphSvg = d3.select('#knowledgeGraphContainer')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'knowledge-graph-svg')
.attr('viewBox', [0, 0, width, height]);
// Create a group for zoom/pan transformations
graphGroup = graphSvg.append('g').attr('class', 'graph-content');
// Setup zoom behavior
graphZoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
graphGroup.attr('transform', event.transform);
});
graphSvg.call(graphZoom);
// Add arrowhead marker
graphSvg.append('defs').append('marker')
.attr('id', 'arrowhead-core')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 20)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
// Create force simulation
graphSimulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(edges).id(d => d.id).distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(20))
.force('x', d3.forceX(width / 2).strength(0.05))
.force('y', d3.forceY(height / 2).strength(0.05));
// Draw edges
const link = graphGroup.append('g')
.attr('class', 'graph-links')
.selectAll('line')
.data(edges)
.enter()
.append('line')
.attr('class', 'graph-edge')
.attr('stroke', '#999')
.attr('stroke-width', 2)
.attr('marker-end', 'url(#arrowhead-core)');
// Draw nodes
const node = graphGroup.append('g')
.attr('class', 'graph-nodes')
.selectAll('g')
.data(nodes)
.enter()
.append('g')
.attr('class', d => 'graph-node-group ' + (d.type || 'entity'))
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
.on('click', (event, d) => {
event.stopPropagation();
showNodeDetail(d);
});
// Add circles to nodes (color by type)
node.append('circle')
.attr('class', d => 'graph-node ' + (d.type || 'entity'))
.attr('r', 10)
.attr('fill', d => {
if (d.type === 'file') return '#3b82f6'; // blue
if (d.type === 'function') return '#10b981'; // green
if (d.type === 'module') return '#8b5cf6'; // purple
return '#6b7280'; // gray
})
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.attr('data-id', d => d.id);
// Add labels to nodes
node.append('text')
.attr('class', 'graph-label')
.text(d => d.displayName)
.attr('x', 14)
.attr('y', 4)
.attr('font-size', '11px')
.attr('fill', '#333');
// Update positions on simulation tick
graphSimulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
});
// Drag functions
function dragstarted(event, d) {
if (!event.active) graphSimulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) graphSimulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
function showNodeDetail(node) {
showNotification(`${node.name} (${node.type})`, 'info');
}
async function viewEvolutionHistory(memoryId) {
try {
const response = await fetch(`/api/core-memory/memories/${memoryId}/evolution?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const versions = await response.json();
const modal = document.getElementById('memoryDetailModal');
document.getElementById('memoryDetailTitle').textContent = `${t('coreMemory.evolutionHistory')} - ${memoryId}`;
const body = document.getElementById('memoryDetailBody');
body.innerHTML = `
<div class="evolution-timeline">
${versions && versions.length > 0
? versions.map((version, index) => renderEvolutionVersion(version, index)).join('')
: `<div class="evolution-empty-state">
<i data-lucide="git-branch"></i>
<p>${t('coreMemory.noHistory')}</p>
</div>`
}
</div>
`;
modal.style.display = 'flex';
lucide.createIcons();
} catch (error) {
console.error('Failed to fetch evolution history:', error);
showNotification(t('coreMemory.evolutionError'), 'error');
}
}
function renderEvolutionVersion(version, index) {
const timestamp = new Date(version.timestamp).toLocaleString();
const contentPreview = version.content
? (version.content.substring(0, 150) + (version.content.length > 150 ? '...' : ''))
: '';
// Parse diff stats
const diffStats = version.diff_stats || {};
const added = diffStats.added || 0;
const modified = diffStats.modified || 0;
const deleted = diffStats.deleted || 0;
return `
<div class="version-card">
<div class="version-header">
<div class="version-info">
<span class="version-number">v${version.version}</span>
<span class="version-date">${timestamp}</span>
${index === 0 ? `<span class="badge badge-current">${t('coreMemory.current')}</span>` : ''}
</div>
</div>
${contentPreview ? `
<div class="version-content-preview">
${escapeHtml(contentPreview)}
</div>
` : ''}
${(added > 0 || modified > 0 || deleted > 0) ? `
<div class="version-diff-stats">
${added > 0 ? `<span class="diff-stat diff-added"><i data-lucide="plus"></i> ${added} added</span>` : ''}
${modified > 0 ? `<span class="diff-stat diff-modified"><i data-lucide="edit-3"></i> ${modified} modified</span>` : ''}
${deleted > 0 ? `<span class="diff-stat diff-deleted"><i data-lucide="minus"></i> ${deleted} deleted</span>` : ''}
</div>
` : ''}
${version.reason ? `
<div class="version-reason">
<strong>Reason:</strong> ${escapeHtml(version.reason)}
</div>
` : ''}
</div>
`;
}

View File

@@ -0,0 +1,580 @@
// Core Memory View
// Manages strategic context entries with knowledge graph and evolution tracking
// State for visualization
let graphSvg = null;
let graphGroup = null;
let graphZoom = null;
let graphSimulation = null;
async function renderCoreMemoryView() {
const content = document.getElementById('content');
hideStatsAndSearch();
// Fetch core memories
const archived = false;
const memories = await fetchCoreMemories(archived);
content.innerHTML = `
<div class="core-memory-container">
<!-- Header Actions -->
<div class="core-memory-header">
<div class="header-actions">
<button class="btn btn-primary" onclick="showCreateMemoryModal()">
<i data-lucide="plus"></i>
${t('coreMemory.createNew')}
</button>
<button class="btn btn-secondary" onclick="toggleArchivedMemories()">
<i data-lucide="archive"></i>
<span id="archiveToggleText">${t('coreMemory.showArchived')}</span>
</button>
<button class="btn btn-secondary" onclick="refreshCoreMemories()">
<i data-lucide="refresh-cw"></i>
${t('common.refresh')}
</button>
</div>
<div class="memory-stats">
<div class="stat-item">
<span class="stat-label">${t('coreMemory.totalMemories')}</span>
<span class="stat-value" id="totalMemoriesCount">${memories.length}</span>
</div>
</div>
</div>
<!-- Memories Grid -->
<div class="memories-grid" id="memoriesGrid">
${memories.length === 0
? `<div class="empty-state">
<i data-lucide="brain"></i>
<p>${t('coreMemory.noMemories')}</p>
</div>`
: memories.map(memory => renderMemoryCard(memory)).join('')
}
</div>
</div>
<!-- Create/Edit Memory Modal -->
<div id="memoryModal" class="modal-overlay" style="display: none;">
<div class="modal-content memory-modal">
<div class="modal-header">
<h2 id="memoryModalTitle">${t('coreMemory.createNew')}</h2>
<button class="modal-close" onclick="closeMemoryModal()">
<i data-lucide="x"></i>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>${t('coreMemory.content')}</label>
<textarea
id="memoryContent"
rows="10"
placeholder="${t('coreMemory.contentPlaceholder')}"
></textarea>
</div>
<div class="form-group">
<label>${t('coreMemory.summary')} (${t('common.optional')})</label>
<textarea
id="memorySummary"
rows="3"
placeholder="${t('coreMemory.summaryPlaceholder')}"
></textarea>
</div>
<div class="form-group">
<label>${t('coreMemory.metadata')} (${t('common.optional')})</label>
<input
type="text"
id="memoryMetadata"
placeholder='{"tags": ["strategy", "architecture"], "priority": "high"}'
/>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeMemoryModal()">
${t('common.cancel')}
</button>
<button class="btn btn-primary" onclick="saveMemory()">
${t('common.save')}
</button>
</div>
</div>
</div>
<!-- Memory Detail Modal -->
<div id="memoryDetailModal" class="modal-overlay" style="display: none;">
<div class="modal-content memory-detail-modal">
<div class="modal-header">
<h2 id="memoryDetailTitle"></h2>
<button class="modal-close" onclick="closeMemoryDetailModal()">
<i data-lucide="x"></i>
</button>
</div>
<div class="modal-body" id="memoryDetailBody">
<!-- Content loaded dynamically -->
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeMemoryDetailModal()">
${t('common.close')}
</button>
</div>
</div>
</div>
`;
lucide.createIcons();
}
function renderMemoryCard(memory) {
const createdDate = new Date(memory.created_at).toLocaleString();
const updatedDate = memory.updated_at ? new Date(memory.updated_at).toLocaleString() : createdDate;
const isArchived = memory.archived || false;
const metadata = memory.metadata || {};
const tags = metadata.tags || [];
const priority = metadata.priority || 'medium';
return `
<div class="memory-card ${isArchived ? 'archived' : ''}" data-memory-id="${memory.id}">
<div class="memory-card-header">
<div class="memory-id">
<i data-lucide="bookmark"></i>
<span>${memory.id}</span>
${isArchived ? `<span class="badge badge-archived">${t('common.archived')}</span>` : ''}
${priority !== 'medium' ? `<span class="badge badge-priority-${priority}">${priority}</span>` : ''}
</div>
<div class="memory-actions">
<button class="icon-btn" onclick="viewMemoryDetail('${memory.id}')" title="${t('common.view')}">
<i data-lucide="eye"></i>
</button>
<button class="icon-btn" onclick="editMemory('${memory.id}')" title="${t('common.edit')}">
<i data-lucide="edit"></i>
</button>
${!isArchived
? `<button class="icon-btn" onclick="archiveMemory('${memory.id}')" title="${t('common.archive')}">
<i data-lucide="archive"></i>
</button>`
: `<button class="icon-btn" onclick="unarchiveMemory('${memory.id}')" title="${t('coreMemory.unarchive')}">
<i data-lucide="archive-restore"></i>
</button>`
}
<button class="icon-btn danger" onclick="deleteMemory('${memory.id}')" title="${t('common.delete')}">
<i data-lucide="trash-2"></i>
</button>
</div>
</div>
<div class="memory-content">
${memory.summary
? `<div class="memory-summary">${escapeHtml(memory.summary)}</div>`
: `<div class="memory-preview">${escapeHtml(memory.content.substring(0, 200))}${memory.content.length > 200 ? '...' : ''}</div>`
}
</div>
${tags.length > 0
? `<div class="memory-tags">
${tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('')}
</div>`
: ''
}
<div class="memory-footer">
<div class="memory-meta">
<span title="${t('coreMemory.created')}">
<i data-lucide="calendar"></i>
${createdDate}
</span>
${memory.updated_at
? `<span title="${t('coreMemory.updated')}">
<i data-lucide="clock"></i>
${updatedDate}
</span>`
: ''
}
</div>
<div class="memory-features">
<button class="feature-btn" onclick="generateMemorySummary('${memory.id}')" title="${t('coreMemory.generateSummary')}">
<i data-lucide="sparkles"></i>
${t('coreMemory.summary')}
</button>
<button class="feature-btn" onclick="viewKnowledgeGraph('${memory.id}')" title="${t('coreMemory.knowledgeGraph')}">
<i data-lucide="network"></i>
${t('coreMemory.graph')}
</button>
<button class="feature-btn" onclick="viewEvolutionHistory('${memory.id}')" title="${t('coreMemory.evolution')}">
<i data-lucide="git-branch"></i>
${t('coreMemory.evolution')}
</button>
</div>
</div>
</div>
`;
}
// API Functions
async function fetchCoreMemories(archived = false) {
try {
const response = await fetch(`/api/core-memory/memories?path=${encodeURIComponent(projectPath)}&archived=${archived}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.error('Failed to fetch core memories:', error);
showNotification(t('coreMemory.fetchError'), 'error');
return [];
}
}
async function fetchMemoryById(memoryId) {
try {
const response = await fetch(`/api/core-memory/memories/${memoryId}?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.error('Failed to fetch memory:', error);
showNotification(t('coreMemory.fetchError'), 'error');
return null;
}
}
// Modal Functions
function showCreateMemoryModal() {
const modal = document.getElementById('memoryModal');
document.getElementById('memoryModalTitle').textContent = t('coreMemory.createNew');
document.getElementById('memoryContent').value = '';
document.getElementById('memorySummary').value = '';
document.getElementById('memoryMetadata').value = '';
modal.dataset.editId = '';
modal.style.display = 'flex';
lucide.createIcons();
}
async function editMemory(memoryId) {
const memory = await fetchMemoryById(memoryId);
if (!memory) return;
const modal = document.getElementById('memoryModal');
document.getElementById('memoryModalTitle').textContent = t('coreMemory.edit');
document.getElementById('memoryContent').value = memory.content || '';
document.getElementById('memorySummary').value = memory.summary || '';
document.getElementById('memoryMetadata').value = memory.metadata ? JSON.stringify(memory.metadata, null, 2) : '';
modal.dataset.editId = memoryId;
modal.style.display = 'flex';
lucide.createIcons();
}
function closeMemoryModal() {
document.getElementById('memoryModal').style.display = 'none';
}
async function saveMemory() {
const modal = document.getElementById('memoryModal');
const content = document.getElementById('memoryContent').value.trim();
const summary = document.getElementById('memorySummary').value.trim();
const metadataStr = document.getElementById('memoryMetadata').value.trim();
if (!content) {
showNotification(t('coreMemory.contentRequired'), 'error');
return;
}
let metadata = {};
if (metadataStr) {
try {
metadata = JSON.parse(metadataStr);
} catch (e) {
showNotification(t('coreMemory.invalidMetadata'), 'error');
return;
}
}
const payload = {
content,
summary: summary || undefined,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
path: projectPath
};
const editId = modal.dataset.editId;
if (editId) {
payload.id = editId;
}
try {
const response = await fetch('/api/core-memory/memories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
showNotification(editId ? t('coreMemory.updated') : t('coreMemory.created'), 'success');
closeMemoryModal();
await refreshCoreMemories();
} catch (error) {
console.error('Failed to save memory:', error);
showNotification(t('coreMemory.saveError'), 'error');
}
}
async function archiveMemory(memoryId) {
if (!confirm(t('coreMemory.confirmArchive'))) return;
try {
const response = await fetch(`/api/core-memory/memories/${memoryId}/archive?path=${encodeURIComponent(projectPath)}`, {
method: 'POST'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
showNotification(t('coreMemory.archived'), 'success');
await refreshCoreMemories();
} catch (error) {
console.error('Failed to archive memory:', error);
showNotification(t('coreMemory.archiveError'), 'error');
}
}
async function unarchiveMemory(memoryId) {
try {
const memory = await fetchMemoryById(memoryId);
if (!memory) return;
memory.archived = false;
const response = await fetch('/api/core-memory/memories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...memory, path: projectPath })
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
showNotification(t('coreMemory.unarchived'), 'success');
await refreshCoreMemories();
} catch (error) {
console.error('Failed to unarchive memory:', error);
showNotification(t('coreMemory.unarchiveError'), 'error');
}
}
async function deleteMemory(memoryId) {
if (!confirm(t('coreMemory.confirmDelete'))) return;
try {
const response = await fetch(`/api/core-memory/memories/${memoryId}?path=${encodeURIComponent(projectPath)}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
showNotification(t('coreMemory.deleted'), 'success');
await refreshCoreMemories();
} catch (error) {
console.error('Failed to delete memory:', error);
showNotification(t('coreMemory.deleteError'), 'error');
}
}
// Feature Functions
async function generateMemorySummary(memoryId) {
try {
showNotification(t('coreMemory.generatingSummary'), 'info');
const response = await fetch(`/api/core-memory/memories/${memoryId}/summary?path=${encodeURIComponent(projectPath)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool: 'gemini' })
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
showNotification(t('coreMemory.summaryGenerated'), 'success');
// Show summary in detail modal
await viewMemoryDetail(memoryId);
} catch (error) {
console.error('Failed to generate summary:', error);
showNotification(t('coreMemory.summaryError'), 'error');
}
}
async function viewKnowledgeGraph(memoryId) {
try {
const response = await fetch(`/api/core-memory/memories/${memoryId}/knowledge-graph?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const graph = await response.json();
const modal = document.getElementById('memoryDetailModal');
document.getElementById('memoryDetailTitle').textContent = `${t('coreMemory.knowledgeGraph')} - ${memoryId}`;
const body = document.getElementById('memoryDetailBody');
body.innerHTML = `
<div class="knowledge-graph">
<div class="graph-section">
<h3>${t('coreMemory.entities')}</h3>
<div class="entities-list">
${graph.entities && graph.entities.length > 0
? graph.entities.map(entity => `
<div class="entity-item">
<span class="entity-name">${escapeHtml(entity.name)}</span>
<span class="entity-type">${escapeHtml(entity.type)}</span>
</div>
`).join('')
: `<p class="empty-text">${t('coreMemory.noEntities')}</p>`
}
</div>
</div>
<div class="graph-section">
<h3>${t('coreMemory.relationships')}</h3>
<div class="relationships-list">
${graph.relationships && graph.relationships.length > 0
? graph.relationships.map(rel => `
<div class="relationship-item">
<span class="rel-source">${escapeHtml(rel.source)}</span>
<span class="rel-type">${escapeHtml(rel.type)}</span>
<span class="rel-target">${escapeHtml(rel.target)}</span>
</div>
`).join('')
: `<p class="empty-text">${t('coreMemory.noRelationships')}</p>`
}
</div>
</div>
</div>
`;
modal.style.display = 'flex';
lucide.createIcons();
} catch (error) {
console.error('Failed to fetch knowledge graph:', error);
showNotification(t('coreMemory.graphError'), 'error');
}
}
async function viewEvolutionHistory(memoryId) {
try {
const response = await fetch(`/api/core-memory/memories/${memoryId}/evolution?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const versions = await response.json();
const modal = document.getElementById('memoryDetailModal');
document.getElementById('memoryDetailTitle').textContent = `${t('coreMemory.evolutionHistory')} - ${memoryId}`;
const body = document.getElementById('memoryDetailBody');
body.innerHTML = `
<div class="evolution-timeline">
${versions && versions.length > 0
? versions.map((version, index) => `
<div class="evolution-version">
<div class="version-header">
<span class="version-number">v${version.version}</span>
<span class="version-date">${new Date(version.timestamp).toLocaleString()}</span>
</div>
<div class="version-reason">${escapeHtml(version.reason || t('coreMemory.noReason'))}</div>
${index === 0 ? `<span class="badge badge-current">${t('coreMemory.current')}</span>` : ''}
</div>
`).join('')
: `<p class="empty-text">${t('coreMemory.noHistory')}</p>`
}
</div>
`;
modal.style.display = 'flex';
lucide.createIcons();
} catch (error) {
console.error('Failed to fetch evolution history:', error);
showNotification(t('coreMemory.evolutionError'), 'error');
}
}
async function viewMemoryDetail(memoryId) {
const memory = await fetchMemoryById(memoryId);
if (!memory) return;
const modal = document.getElementById('memoryDetailModal');
document.getElementById('memoryDetailTitle').textContent = memory.id;
const body = document.getElementById('memoryDetailBody');
body.innerHTML = `
<div class="memory-detail-content">
${memory.summary
? `<div class="detail-section">
<h3>${t('coreMemory.summary')}</h3>
<div class="detail-text">${escapeHtml(memory.summary)}</div>
</div>`
: ''
}
<div class="detail-section">
<h3>${t('coreMemory.content')}</h3>
<pre class="detail-code">${escapeHtml(memory.content)}</pre>
</div>
${memory.metadata && Object.keys(memory.metadata).length > 0
? `<div class="detail-section">
<h3>${t('coreMemory.metadata')}</h3>
<pre class="detail-code">${escapeHtml(JSON.stringify(memory.metadata, null, 2))}</pre>
</div>`
: ''
}
${memory.raw_output
? `<div class="detail-section">
<h3>${t('coreMemory.rawOutput')}</h3>
<pre class="detail-code">${escapeHtml(memory.raw_output)}</pre>
</div>`
: ''
}
</div>
`;
modal.style.display = 'flex';
lucide.createIcons();
}
function closeMemoryDetailModal() {
document.getElementById('memoryDetailModal').style.display = 'none';
}
let showingArchivedMemories = false;
async function toggleArchivedMemories() {
showingArchivedMemories = !showingArchivedMemories;
const toggleText = document.getElementById('archiveToggleText');
toggleText.textContent = showingArchivedMemories
? t('coreMemory.showActive')
: t('coreMemory.showArchived');
await refreshCoreMemories();
}
async function refreshCoreMemories() {
const memories = await fetchCoreMemories(showingArchivedMemories);
const grid = document.getElementById('memoriesGrid');
const countEl = document.getElementById('totalMemoriesCount');
if (countEl) countEl.textContent = memories.length;
if (memories.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<i data-lucide="brain"></i>
<p>${showingArchivedMemories ? t('coreMemory.noArchivedMemories') : t('coreMemory.noMemories')}</p>
</div>
`;
} else {
grid.innerHTML = memories.map(memory => renderMemoryCard(memory)).join('');
}
lucide.createIcons();
}
// Utility Functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

View File

@@ -5,6 +5,7 @@
var graphData = { nodes: [], edges: [] };
var cyInstance = null;
var activeTab = 'graph';
var activeDataSource = 'code';
var nodeFilters = {
MODULE: true,
CLASS: true,
@@ -90,6 +91,43 @@ async function loadGraphData() {
}
}
async function loadCoreMemoryGraphData() {
try {
var response = await fetch('/api/core-memory/graph');
if (!response.ok) throw new Error('Failed to load core memory graph data');
var data = await response.json();
graphData = {
nodes: (data.nodes || []).map(function(node) {
return {
id: node.id || node.name,
name: node.name || node.label || node.id,
type: node.type || 'MODULE',
symbolType: node.symbol_type || node.symbolType,
path: node.path || node.file_path,
lineNumber: node.line_number || node.lineNumber,
imports: node.imports || 0,
exports: node.exports || 0,
references: node.references || 0
};
}),
edges: (data.edges || []).map(function(edge) {
return {
source: edge.source || edge.from,
target: edge.target || edge.to,
type: edge.type || edge.relation_type || 'CALLS',
weight: edge.weight || 1
};
})
};
return graphData;
} catch (err) {
console.error('Failed to load core memory graph data:', err);
graphData = { nodes: [], edges: [] };
return graphData;
}
}
async function loadSearchProcessData() {
try {
var response = await fetch('/api/graph/search-process');
@@ -154,6 +192,10 @@ function renderGraphView() {
'<i data-lucide="arrow-right" class="w-3 h-3"></i> ' +
graphData.edges.length + ' ' + t('graph.edges') +
'</span>' +
'<select id="dataSourceSelect" onchange="switchDataSource(this.value)" class="data-source-select">' +
'<option value="code" ' + (activeDataSource === 'code' ? 'selected' : '') + '>' + t('graph.codeRelations') + '</option>' +
'<option value="memory" ' + (activeDataSource === 'memory' ? 'selected' : '') + '>' + t('graph.coreMemory') + '</option>' +
'</select>' +
'</div>' +
'<div class="graph-toolbar-right">' +
'<button class="btn-icon" onclick="fitCytoscape()" title="' + t('graph.fitView') + '">' +
@@ -715,13 +757,62 @@ function closeImpactModal() {
}
}
// ========== Data Source Switching ==========
async function switchDataSource(source) {
activeDataSource = source;
// Show loading state
var container = document.getElementById('cytoscapeContainer');
if (container) {
container.innerHTML = '<div class="cytoscape-empty">' +
'<i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i>' +
'<p>' + t('common.loading') + '</p>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
// Load data based on source
if (source === 'memory') {
await loadCoreMemoryGraphData();
} else {
await loadGraphData();
}
// Update stats display
var statsSpans = document.querySelectorAll('.graph-stats');
if (statsSpans.length >= 2) {
statsSpans[0].innerHTML = '<i data-lucide="circle" class="w-3 h-3"></i> ' +
graphData.nodes.length + ' ' + t('graph.nodes');
statsSpans[1].innerHTML = '<i data-lucide="arrow-right" class="w-3 h-3"></i> ' +
graphData.edges.length + ' ' + t('graph.edges');
if (window.lucide) lucide.createIcons();
}
// Refresh Cytoscape with new data
if (cyInstance) {
refreshCytoscape();
} else {
initializeCytoscape();
}
// Show toast notification
if (window.showToast) {
var sourceName = source === 'memory' ? t('graph.coreMemory') : t('graph.codeRelations');
showToast(t('graph.dataSourceSwitched') + ': ' + sourceName, 'success');
}
}
// ========== Data Refresh ==========
async function refreshGraphData() {
if (window.showToast) {
showToast(t('common.refreshing'), 'info');
}
await loadGraphData();
if (activeDataSource === 'memory') {
await loadCoreMemoryGraphData();
} else {
await loadGraphData();
}
if (activeTab === 'graph' && cyInstance) {
refreshCytoscape();

View File

@@ -424,6 +424,10 @@
<i data-lucide="database" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.contextMemory">Context</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="core-memory" data-tooltip="Core Memory">
<i data-lucide="brain" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.coreMemory">Core Memory</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="prompt-history" data-tooltip="Prompt History">
<i data-lucide="message-square" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.promptHistory">Prompts</span>
@@ -537,11 +541,6 @@
</div>
</div>
<!-- Storage Manager Card (only visible in CLI Manager view) -->
<section id="storageCard" class="mb-6" style="display: none;">
<!-- Rendered by storage-manager.js -->
</section>
<!-- Main Content Container -->
<section class="main-content" id="mainContent">
<!-- Dynamic content: sessions grid or session detail page -->

View File

@@ -0,0 +1,236 @@
/**
* Core Memory Tool - MCP tool for core memory management
* Operations: list, import, export, summary
*/
import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js';
import { getCoreMemoryStore } from '../core/core-memory-store.js';
// Zod schemas
const OperationEnum = z.enum(['list', 'import', 'export', 'summary']);
const ParamsSchema = z.object({
operation: OperationEnum,
text: z.string().optional(),
id: z.string().optional(),
tool: z.enum(['gemini', 'qwen']).optional().default('gemini'),
limit: z.number().optional().default(100),
});
type Params = z.infer<typeof ParamsSchema>;
interface CoreMemory {
id: string;
content: string;
summary: string | null;
archived: boolean;
created_at: string;
updated_at: string;
}
interface ListResult {
operation: 'list';
memories: CoreMemory[];
total: number;
}
interface ImportResult {
operation: 'import';
id: string;
message: string;
}
interface ExportResult {
operation: 'export';
id: string;
content: string;
}
interface SummaryResult {
operation: 'summary';
id: string;
summary: string;
}
type OperationResult = ListResult | ImportResult | ExportResult | SummaryResult;
/**
* Get project path from current working directory
*/
function getProjectPath(): string {
return process.cwd();
}
/**
* Operation: list
* List all memories
*/
function executeList(params: Params): ListResult {
const { limit } = params;
const store = getCoreMemoryStore(getProjectPath());
const memories = store.getMemories({ limit }) as CoreMemory[];
return {
operation: 'list',
memories,
total: memories.length,
};
}
/**
* Operation: import
* Import text as a new memory
*/
function executeImport(params: Params): ImportResult {
const { text } = params;
if (!text || text.trim() === '') {
throw new Error('Parameter "text" is required for import operation');
}
const store = getCoreMemoryStore(getProjectPath());
const memory = store.upsertMemory({
content: text.trim(),
});
// Extract knowledge graph
store.extractKnowledgeGraph(memory.id);
return {
operation: 'import',
id: memory.id,
message: `Created memory: ${memory.id}`,
};
}
/**
* Operation: export
* Export a memory as plain text
*/
function executeExport(params: Params): ExportResult {
const { id } = params;
if (!id) {
throw new Error('Parameter "id" is required for export operation');
}
const store = getCoreMemoryStore(getProjectPath());
const memory = store.getMemory(id);
if (!memory) {
throw new Error(`Memory "${id}" not found`);
}
return {
operation: 'export',
id,
content: memory.content,
};
}
/**
* Operation: summary
* Generate AI summary for a memory
*/
async function executeSummary(params: Params): Promise<SummaryResult> {
const { id, tool = 'gemini' } = params;
if (!id) {
throw new Error('Parameter "id" is required for summary operation');
}
const store = getCoreMemoryStore(getProjectPath());
const memory = store.getMemory(id);
if (!memory) {
throw new Error(`Memory "${id}" not found`);
}
const summary = await store.generateSummary(id, tool);
return {
operation: 'summary',
id,
summary,
};
}
/**
* Route to appropriate operation handler
*/
async function execute(params: Params): Promise<OperationResult> {
const { operation } = params;
switch (operation) {
case 'list':
return executeList(params);
case 'import':
return executeImport(params);
case 'export':
return executeExport(params);
case 'summary':
return executeSummary(params);
default:
throw new Error(
`Unknown operation: ${operation}. Valid operations: list, import, export, summary`
);
}
}
// Tool schema for MCP
export const schema: ToolSchema = {
name: 'core_memory',
description: `Core memory management for strategic context.
Usage:
core_memory(operation="list") # List all memories
core_memory(operation="import", text="important context") # Import text as new memory
core_memory(operation="export", id="CMEM-xxx") # Export memory as plain text
core_memory(operation="summary", id="CMEM-xxx") # Generate AI summary
Memory IDs use format: CMEM-YYYYMMDD-HHMMSS`,
inputSchema: {
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['list', 'import', 'export', 'summary'],
description: 'Operation to perform',
},
text: {
type: 'string',
description: 'Text content to import (required for import operation)',
},
id: {
type: 'string',
description: 'Memory ID (required for export/summary operations)',
},
tool: {
type: 'string',
enum: ['gemini', 'qwen'],
description: 'AI tool for summary generation (default: gemini)',
},
limit: {
type: 'number',
description: 'Max number of memories to list (default: 100)',
},
},
required: ['operation'],
},
};
// Handler function
export async function handler(params: Record<string, unknown>): Promise<ToolResult<OperationResult>> {
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return { success: false, error: `Invalid params: ${parsed.error.message}` };
}
try {
const result = await execute(parsed.data);
return { success: true, result };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}

View File

@@ -21,6 +21,7 @@ import * as smartSearchMod from './smart-search.js';
import { executeInitWithProgress } from './smart-search.js';
// codex_lens removed - functionality integrated into smart_search
import * as readFileMod from './read-file.js';
import * as coreMemoryMod from './core-memory.js';
import type { ProgressInfo } from './codex-lens.js';
// Import legacy JS tools
@@ -355,6 +356,7 @@ registerTool(toLegacyTool(cliExecutorMod));
registerTool(toLegacyTool(smartSearchMod));
// codex_lens removed - functionality integrated into smart_search
registerTool(toLegacyTool(readFileMod));
registerTool(toLegacyTool(coreMemoryMod));
// Register legacy JS tools
registerTool(uiGeneratePreviewTool);