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