mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +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:
291
.claude/commands/memory/compact.md
Normal file
291
.claude/commands/memory/compact.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
---
|
||||||
|
name: compact
|
||||||
|
description: Compact current session memory into structured text for session recovery, extracting objective/plan/files/decisions/constraints/state, and save via MCP core_memory tool
|
||||||
|
argument-hint: "[optional: session description]"
|
||||||
|
allowed-tools: mcp__ccw-tools__core_memory(*), Read(*)
|
||||||
|
examples:
|
||||||
|
- /memory:compact
|
||||||
|
- /memory:compact "completed core-memory module"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Memory Compact Command (/memory:compact)
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
The `memory:compact` command **compresses current session working memory** into structured text optimized for **session recovery**, extracts critical information, and saves it to persistent storage via MCP `core_memory` tool.
|
||||||
|
|
||||||
|
**Core Philosophy**:
|
||||||
|
- **Session Recovery First**: Capture everything needed to resume work seamlessly
|
||||||
|
- **Minimize Re-exploration**: Include file paths, decisions, and state to avoid redundant analysis
|
||||||
|
- **Preserve Train of Thought**: Keep notes and hypotheses for complex debugging
|
||||||
|
- **Actionable State**: Record last action result and known issues
|
||||||
|
|
||||||
|
## 2. Parameters
|
||||||
|
|
||||||
|
- `"session description"` (Optional): Session description to supplement objective
|
||||||
|
- Example: "completed core-memory module"
|
||||||
|
- Example: "debugging JWT refresh - suspected memory leak"
|
||||||
|
|
||||||
|
## 3. Structured Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Objective
|
||||||
|
[High-level goal - the "North Star" of this session]
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
- [x] [Completed step]
|
||||||
|
- [x] [Completed step]
|
||||||
|
- [ ] [Pending step]
|
||||||
|
|
||||||
|
## Active Files
|
||||||
|
- path/to/file1.ts (role: main implementation)
|
||||||
|
- path/to/file2.ts (role: tests)
|
||||||
|
|
||||||
|
## Last Action
|
||||||
|
[Last significant action and its result/status]
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- [Decision]: [Reasoning]
|
||||||
|
- [Decision]: [Reasoning]
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- [User-specified limitation or preference]
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- [Added/changed packages or environment requirements]
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
- [Deferred bug or edge case]
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
- [Completed modification]
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
- [Next step] or (none)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
[Unstructured thoughts, hypotheses, debugging trails]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Field Definitions
|
||||||
|
|
||||||
|
| Field | Purpose | Recovery Value |
|
||||||
|
|-------|---------|----------------|
|
||||||
|
| **Objective** | Ultimate goal of the session | Prevents losing track of broader feature |
|
||||||
|
| **Plan** | Steps with status markers | Avoids re-planning or repeating steps |
|
||||||
|
| **Active Files** | Working set of files | Eliminates re-exploration of codebase |
|
||||||
|
| **Last Action** | Final tool output/status | Immediate state awareness (success/failure) |
|
||||||
|
| **Decisions** | Architectural choices + reasoning | Prevents re-litigating settled decisions |
|
||||||
|
| **Constraints** | User-imposed limitations | Maintains personalized coding style |
|
||||||
|
| **Dependencies** | Package/environment changes | Prevents missing dependency errors |
|
||||||
|
| **Known Issues** | Deferred bugs/edge cases | Ensures issues aren't forgotten |
|
||||||
|
| **Changes Made** | Completed modifications | Clear record of what was done |
|
||||||
|
| **Pending** | Next steps | Immediate action items |
|
||||||
|
| **Notes** | Hypotheses, debugging trails | Preserves "train of thought" |
|
||||||
|
|
||||||
|
## 5. Execution Flow
|
||||||
|
|
||||||
|
### Step 1: Analyze Current Session
|
||||||
|
|
||||||
|
Extract the following from conversation history:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const sessionAnalysis = {
|
||||||
|
objective: "", // High-level goal (1-2 sentences)
|
||||||
|
plan: [], // Steps with status: {step, status: 'done'|'pending'}
|
||||||
|
activeFiles: [], // {path, role} - working set
|
||||||
|
lastAction: "", // Last significant action + result
|
||||||
|
decisions: [], // {decision, reasoning}
|
||||||
|
constraints: [], // User-specified limitations
|
||||||
|
dependencies: [], // Added/changed packages
|
||||||
|
knownIssues: [], // Deferred bugs
|
||||||
|
changesMade: [], // Completed modifications
|
||||||
|
pending: [], // Next steps
|
||||||
|
notes: "" // Unstructured thoughts
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Generate Structured Text
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const structuredText = `## Objective
|
||||||
|
${sessionAnalysis.objective}
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
${sessionAnalysis.plan.map(p =>
|
||||||
|
`- [${p.status === 'done' ? 'x' : ' '}] ${p.step}`
|
||||||
|
).join('\n')}
|
||||||
|
|
||||||
|
## Active Files
|
||||||
|
${sessionAnalysis.activeFiles.map(f => `- ${f.path} (${f.role})`).join('\n')}
|
||||||
|
|
||||||
|
## Last Action
|
||||||
|
${sessionAnalysis.lastAction}
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
${sessionAnalysis.decisions.map(d => `- ${d.decision}: ${d.reasoning}`).join('\n') || '(none)'}
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
${sessionAnalysis.constraints.map(c => `- ${c}`).join('\n') || '(none)'}
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
${sessionAnalysis.dependencies.map(d => `- ${d}`).join('\n') || '(none)'}
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
${sessionAnalysis.knownIssues.map(i => `- ${i}`).join('\n') || '(none)'}
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
${sessionAnalysis.changesMade.map(c => `- ${c}`).join('\n')}
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
${sessionAnalysis.pending.length > 0
|
||||||
|
? sessionAnalysis.pending.map(p => `- ${p}`).join('\n')
|
||||||
|
: '(none)'}
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
${sessionAnalysis.notes || '(none)'}`
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Import to Core Memory via MCP
|
||||||
|
|
||||||
|
Use the MCP `core_memory` tool to save the structured text:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
mcp__ccw-tools__core_memory({
|
||||||
|
operation: "import",
|
||||||
|
text: structuredText
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"operation": "import",
|
||||||
|
"id": "CMEM-YYYYMMDD-HHMMSS",
|
||||||
|
"message": "Created memory: CMEM-YYYYMMDD-HHMMSS"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Report Recovery ID
|
||||||
|
|
||||||
|
After successful import, **clearly display the Recovery ID** to the user:
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════╗
|
||||||
|
║ ✓ Session Memory Saved ║
|
||||||
|
║ ║
|
||||||
|
║ Recovery ID: CMEM-YYYYMMDD-HHMMSS ║
|
||||||
|
║ ║
|
||||||
|
║ To restore this session in a new conversation: ║
|
||||||
|
║ > Use MCP: core_memory(operation="export", id="<ID>") ║
|
||||||
|
║ > Or CLI: ccw core-memory export --id <ID> ║
|
||||||
|
╚══════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Usage Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/memory:compact
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```markdown
|
||||||
|
## Objective
|
||||||
|
Add core-memory module to ccw for persistent memory management with knowledge graph visualization
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
- [x] Create CoreMemoryStore with SQLite backend
|
||||||
|
- [x] Implement RESTful API routes (/api/core-memory/*)
|
||||||
|
- [x] Build frontend three-column view
|
||||||
|
- [x] Simplify CLI to 4 commands
|
||||||
|
- [x] Extend graph-explorer with data source switch
|
||||||
|
|
||||||
|
## Active Files
|
||||||
|
- ccw/src/core/core-memory-store.ts (storage layer)
|
||||||
|
- ccw/src/core/routes/core-memory-routes.ts (API)
|
||||||
|
- ccw/src/commands/core-memory.ts (CLI)
|
||||||
|
- ccw/src/templates/dashboard-js/views/core-memory.js (frontend)
|
||||||
|
|
||||||
|
## Last Action
|
||||||
|
TypeScript build succeeded with no errors
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- Independent storage: Avoid conflicts with existing memory-store.ts
|
||||||
|
- Timestamp-based ID (CMEM-YYYYMMDD-HHMMSS): Human-readable and sortable
|
||||||
|
- Extend graph-explorer: Reuse existing Cytoscape infrastructure
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- CLI must be simple: only list/import/export/summary commands
|
||||||
|
- Import/export use plain text, not files
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- No new packages added (uses existing better-sqlite3)
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
- N+1 query in graph aggregation (acceptable for initial scale)
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
- Created 4 new files (store, routes, CLI, frontend view)
|
||||||
|
- Modified server.ts, navigation.js, i18n.js
|
||||||
|
- Added /memory:compact slash command
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
(none)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
User prefers minimal CLI design. Graph aggregation can be optimized with JOIN query if memory count grows.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**:
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════╗
|
||||||
|
║ ✓ Session Memory Saved ║
|
||||||
|
║ ║
|
||||||
|
║ Recovery ID: CMEM-20251218-150322 ║
|
||||||
|
║ ║
|
||||||
|
║ To restore this session in a new conversation: ║
|
||||||
|
║ > Use MCP: core_memory(operation="export", id="<ID>") ║
|
||||||
|
║ > Or CLI: ccw core-memory export --id <ID> ║
|
||||||
|
╚══════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Recovery Usage
|
||||||
|
|
||||||
|
When starting a new session, load previous context using MCP tools:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// List available memories
|
||||||
|
mcp__ccw-tools__core_memory({ operation: "list" })
|
||||||
|
|
||||||
|
// Export and read previous session
|
||||||
|
mcp__ccw-tools__core_memory({ operation: "export", id: "CMEM-20251218-150322" })
|
||||||
|
|
||||||
|
// Or generate AI summary for quick context
|
||||||
|
mcp__ccw-tools__core_memory({ operation: "summary", id: "CMEM-20251218-150322" })
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccw core-memory list
|
||||||
|
ccw core-memory export --id CMEM-20251218-150322
|
||||||
|
ccw core-memory summary --id CMEM-20251218-150322
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Quality Checklist
|
||||||
|
|
||||||
|
Before generating:
|
||||||
|
- [ ] Objective clearly states the "North Star" goal
|
||||||
|
- [ ] Plan shows completion status with [x] / [ ] markers
|
||||||
|
- [ ] Active Files includes 3-8 core files with roles
|
||||||
|
- [ ] Last Action captures final state (success/failure)
|
||||||
|
- [ ] Decisions include reasoning, not just choices
|
||||||
|
- [ ] Known Issues separates deferred from forgotten bugs
|
||||||
|
- [ ] Notes preserve debugging hypotheses if any
|
||||||
|
|
||||||
|
## 9. Notes
|
||||||
|
|
||||||
|
- **Timing**: Execute at task completion or before context switch
|
||||||
|
- **Frequency**: Once per independent task or milestone
|
||||||
|
- **Recovery**: New session can immediately continue with full context
|
||||||
|
- **Knowledge Graph**: Entity relationships auto-extracted for visualization
|
||||||
38
.claude/workflows/chinese-response.md
Normal file
38
.claude/workflows/chinese-response.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 中文回复准则
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
- **语言一致性**:所有回复必须使用中文(简体)
|
||||||
|
- **专业术语**:技术术语可保留英文原文,但需添加中文解释
|
||||||
|
- **代码注释**:代码注释使用英文(保持代码的国际化兼容性)
|
||||||
|
|
||||||
|
## 回复格式
|
||||||
|
|
||||||
|
### 一般对话
|
||||||
|
- 使用简洁、清晰的中文表达
|
||||||
|
- 避免过度使用网络用语或口语化表达
|
||||||
|
- 保持专业、礼貌的语气
|
||||||
|
|
||||||
|
### 技术讨论
|
||||||
|
- 技术概念首次出现时可用「中文(English)」格式
|
||||||
|
- 示例:依赖注入(Dependency Injection)
|
||||||
|
- 后续可直接使用中文或英文缩写
|
||||||
|
|
||||||
|
### 代码相关
|
||||||
|
- 代码块内容保持英文(变量名、注释等)
|
||||||
|
- 代码解释使用中文
|
||||||
|
- 文件路径、命令等保持原样
|
||||||
|
|
||||||
|
## 格式规范
|
||||||
|
|
||||||
|
- 中英文之间添加空格:`使用 TypeScript 开发`
|
||||||
|
- 数字与中文之间添加空格:`共有 3 个文件`
|
||||||
|
- 标点符号使用中文全角标点:`,。!?:;`
|
||||||
|
- 引用代码或命令时使用反引号:`npm install`
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 不要在代码文件中写入中文(保持代码的国际化)
|
||||||
|
- 错误信息和日志保持英文
|
||||||
|
- Git commit 信息保持英文
|
||||||
|
- 文档文件可根据需要使用中文
|
||||||
@@ -10,6 +10,7 @@ import { toolCommand } from './commands/tool.js';
|
|||||||
import { sessionCommand } from './commands/session.js';
|
import { sessionCommand } from './commands/session.js';
|
||||||
import { cliCommand } from './commands/cli.js';
|
import { cliCommand } from './commands/cli.js';
|
||||||
import { memoryCommand } from './commands/memory.js';
|
import { memoryCommand } from './commands/memory.js';
|
||||||
|
import { coreMemoryCommand } from './commands/core-memory.js';
|
||||||
import { readFileSync, existsSync } from 'fs';
|
import { readFileSync, existsSync } from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
@@ -193,5 +194,20 @@ export function run(argv: string[]): void {
|
|||||||
.option('--dry-run', 'Preview without deleting')
|
.option('--dry-run', 'Preview without deleting')
|
||||||
.action((subcommand, args, options) => memoryCommand(subcommand, args, options));
|
.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);
|
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',
|
'12-skills-rules.css',
|
||||||
'13-claude-manager.css',
|
'13-claude-manager.css',
|
||||||
'14-graph-explorer.css',
|
'14-graph-explorer.css',
|
||||||
'15-mcp-manager.css'
|
'15-mcp-manager.css',
|
||||||
|
'16-help.css',
|
||||||
|
'17-core-memory.css'
|
||||||
];
|
];
|
||||||
|
|
||||||
const MODULE_FILES = [
|
const MODULE_FILES = [
|
||||||
@@ -56,6 +58,8 @@ const MODULE_FILES = [
|
|||||||
'components/mcp-manager.js',
|
'components/mcp-manager.js',
|
||||||
'components/hook-manager.js',
|
'components/hook-manager.js',
|
||||||
'components/version-check.js',
|
'components/version-check.js',
|
||||||
|
'components/storage-manager.js',
|
||||||
|
'components/index-manager.js',
|
||||||
'views/home.js',
|
'views/home.js',
|
||||||
'views/project-overview.js',
|
'views/project-overview.js',
|
||||||
'views/session-detail.js',
|
'views/session-detail.js',
|
||||||
@@ -69,6 +73,14 @@ const MODULE_FILES = [
|
|||||||
'views/hook-manager.js',
|
'views/hook-manager.js',
|
||||||
'views/history.js',
|
'views/history.js',
|
||||||
'views/graph-explorer.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'
|
'main.js'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -800,5 +800,127 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
return true;
|
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;
|
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 { handleStatusRoutes } from './routes/status-routes.js';
|
||||||
import { handleCliRoutes } from './routes/cli-routes.js';
|
import { handleCliRoutes } from './routes/cli-routes.js';
|
||||||
import { handleMemoryRoutes } from './routes/memory-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 { handleMcpRoutes } from './routes/mcp-routes.js';
|
||||||
import { handleHooksRoutes } from './routes/hooks-routes.js';
|
import { handleHooksRoutes } from './routes/hooks-routes.js';
|
||||||
import { handleCodexLensRoutes } from './routes/codexlens-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;
|
if (await handleCliRoutes(routeContext)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claude CLAUDE.md routes (/api/memory/claude/*)
|
// Claude CLAUDE.md routes (/api/memory/claude/*) and Language routes (/api/language/*)
|
||||||
if (pathname.startsWith('/api/memory/claude/')) {
|
if (pathname.startsWith('/api/memory/claude/') || pathname.startsWith('/api/language/')) {
|
||||||
if (await handleClaudeRoutes(routeContext)) return;
|
if (await handleClaudeRoutes(routeContext)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +270,12 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
if (await handleMemoryRoutes(routeContext)) return;
|
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*)
|
// MCP routes (/api/mcp*, /api/codex-mcp*)
|
||||||
if (pathname.startsWith('/api/mcp') || pathname.startsWith('/api/codex-mcp')) {
|
if (pathname.startsWith('/api/mcp') || pathname.startsWith('/api/codex-mcp')) {
|
||||||
if (await handleMcpRoutes(routeContext)) return;
|
if (await handleMcpRoutes(routeContext)) return;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const SERVER_NAME = 'ccw-tools';
|
|||||||
const SERVER_VERSION = '6.1.4';
|
const SERVER_VERSION = '6.1.4';
|
||||||
|
|
||||||
// Default enabled tools (core set)
|
// 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
|
* Get list of enabled tools from environment or defaults
|
||||||
|
|||||||
@@ -3552,6 +3552,29 @@
|
|||||||
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.5);
|
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 */
|
/* Ghost button variant for destructive actions */
|
||||||
.btn-ghost.text-destructive {
|
.btn-ghost.text-destructive {
|
||||||
color: hsl(var(--destructive));
|
color: hsl(var(--destructive));
|
||||||
|
|||||||
@@ -1285,6 +1285,30 @@
|
|||||||
color: hsl(var(--muted-foreground));
|
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 {
|
.btn-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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();
|
renderGraphExplorer();
|
||||||
} else if (currentView === 'help') {
|
} else if (currentView === 'help') {
|
||||||
renderHelpView();
|
renderHelpView();
|
||||||
|
} else if (currentView === 'core-memory') {
|
||||||
|
renderCoreMemoryView();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -175,6 +177,8 @@ function updateContentTitle() {
|
|||||||
titleEl.textContent = t('title.graphExplorer');
|
titleEl.textContent = t('title.graphExplorer');
|
||||||
} else if (currentView === 'help') {
|
} else if (currentView === 'help') {
|
||||||
titleEl.textContent = t('title.helpGuide');
|
titleEl.textContent = t('title.helpGuide');
|
||||||
|
} else if (currentView === 'core-memory') {
|
||||||
|
titleEl.textContent = t('title.coreMemory');
|
||||||
} else if (currentView === 'liteTasks') {
|
} else if (currentView === 'liteTasks') {
|
||||||
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
|
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
|
||||||
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const i18n = {
|
|||||||
'nav.history': 'History',
|
'nav.history': 'History',
|
||||||
'nav.memory': 'Memory',
|
'nav.memory': 'Memory',
|
||||||
'nav.contextMemory': 'Context',
|
'nav.contextMemory': 'Context',
|
||||||
|
'nav.coreMemory': 'Core Memory',
|
||||||
'nav.promptHistory': 'Prompts',
|
'nav.promptHistory': 'Prompts',
|
||||||
|
|
||||||
// Sidebar - Sessions section
|
// Sidebar - Sessions section
|
||||||
@@ -287,6 +288,30 @@ const i18n = {
|
|||||||
'codexlens.indexSuccess': 'Index created successfully',
|
'codexlens.indexSuccess': 'Index created successfully',
|
||||||
'codexlens.indexFailed': 'Indexing failed',
|
'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 Search Configuration
|
||||||
'semantic.settings': 'Semantic Search Settings',
|
'semantic.settings': 'Semantic Search Settings',
|
||||||
'semantic.testSearch': 'Test Semantic Search',
|
'semantic.testSearch': 'Test Semantic Search',
|
||||||
@@ -294,7 +319,19 @@ const i18n = {
|
|||||||
'semantic.runSearch': 'Run Semantic Search',
|
'semantic.runSearch': 'Run Semantic Search',
|
||||||
'semantic.close': 'Close',
|
'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.promptFormat': 'Prompt Format',
|
||||||
'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation',
|
'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation',
|
||||||
'cli.storageBackend': 'Storage Backend',
|
'cli.storageBackend': 'Storage Backend',
|
||||||
@@ -1060,15 +1097,17 @@ const i18n = {
|
|||||||
'graph.searchProcessDesc': 'Visualize how search queries flow through the system',
|
'graph.searchProcessDesc': 'Visualize how search queries flow through the system',
|
||||||
'graph.searchProcessTitle': 'Search Pipeline',
|
'graph.searchProcessTitle': 'Search Pipeline',
|
||||||
'graph.resultsFound': 'results found',
|
'graph.resultsFound': 'results found',
|
||||||
|
'graph.coreMemory': 'Core Memory',
|
||||||
|
'graph.dataSourceSwitched': 'Data source switched',
|
||||||
'graph.type': 'Type',
|
'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.symbolType': 'Symbol Type',
|
||||||
|
'graph.path': 'Path',
|
||||||
|
'graph.line': 'Line',
|
||||||
|
'graph.imports': 'imports',
|
||||||
|
'graph.exports': 'exports',
|
||||||
|
'graph.references': 'references',
|
||||||
'graph.affectedSymbols': 'Affected Symbols',
|
'graph.affectedSymbols': 'Affected Symbols',
|
||||||
|
'graph.depth': 'Depth',
|
||||||
|
|
||||||
// CLI Sync (used in claude-manager.js)
|
// CLI Sync (used in claude-manager.js)
|
||||||
'claude.cliSync': 'CLI Auto-Sync',
|
'claude.cliSync': 'CLI Auto-Sync',
|
||||||
@@ -1131,6 +1170,56 @@ const i18n = {
|
|||||||
'common.saveFailed': 'Failed to save',
|
'common.saveFailed': 'Failed to save',
|
||||||
'common.unknownError': 'Unknown error',
|
'common.unknownError': 'Unknown error',
|
||||||
'common.exception': 'Exception',
|
'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: {
|
zh: {
|
||||||
@@ -1154,6 +1243,7 @@ const i18n = {
|
|||||||
'nav.history': '历史',
|
'nav.history': '历史',
|
||||||
'nav.memory': '记忆',
|
'nav.memory': '记忆',
|
||||||
'nav.contextMemory': '活动',
|
'nav.contextMemory': '活动',
|
||||||
|
'nav.coreMemory': '核心记忆',
|
||||||
'nav.promptHistory': '洞察',
|
'nav.promptHistory': '洞察',
|
||||||
|
|
||||||
// Sidebar - Sessions section
|
// Sidebar - Sessions section
|
||||||
@@ -1412,6 +1502,30 @@ const i18n = {
|
|||||||
'codexlens.indexSuccess': '索引创建成功',
|
'codexlens.indexSuccess': '索引创建成功',
|
||||||
'codexlens.indexFailed': '索引失败',
|
'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 Search 配置
|
||||||
'semantic.settings': '语义搜索设置',
|
'semantic.settings': '语义搜索设置',
|
||||||
'semantic.testSearch': '测试语义搜索',
|
'semantic.testSearch': '测试语义搜索',
|
||||||
@@ -1419,7 +1533,19 @@ const i18n = {
|
|||||||
'semantic.runSearch': '运行语义搜索',
|
'semantic.runSearch': '运行语义搜索',
|
||||||
'semantic.close': '关闭',
|
'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.promptFormat': '提示词格式',
|
||||||
'cli.promptFormatDesc': '多轮对话拼接格式',
|
'cli.promptFormatDesc': '多轮对话拼接格式',
|
||||||
'cli.storageBackend': '存储后端',
|
'cli.storageBackend': '存储后端',
|
||||||
@@ -2163,6 +2289,8 @@ const i18n = {
|
|||||||
'graph.searchProcessDesc': '可视化搜索查询在系统中的流转过程',
|
'graph.searchProcessDesc': '可视化搜索查询在系统中的流转过程',
|
||||||
'graph.searchProcessTitle': '搜索管道',
|
'graph.searchProcessTitle': '搜索管道',
|
||||||
'graph.resultsFound': '个结果',
|
'graph.resultsFound': '个结果',
|
||||||
|
'graph.coreMemory': '核心记忆',
|
||||||
|
'graph.dataSourceSwitched': '数据源已切换',
|
||||||
'graph.type': '类型',
|
'graph.type': '类型',
|
||||||
'graph.line': '行号',
|
'graph.line': '行号',
|
||||||
'graph.path': '路径',
|
'graph.path': '路径',
|
||||||
@@ -2265,6 +2393,56 @@ const i18n = {
|
|||||||
'common.saveFailed': '保存失败',
|
'common.saveFailed': '保存失败',
|
||||||
'common.unknownError': '未知错误',
|
'common.unknownError': '未知错误',
|
||||||
'common.exception': '异常',
|
'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 (statsGrid) statsGrid.style.display = 'none';
|
||||||
if (searchInput) searchInput.parentElement.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)
|
// Load data (including CodexLens status for tools section)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadCliToolStatus(),
|
loadCliToolStatus(),
|
||||||
@@ -314,13 +310,17 @@ async function renderCliManager() {
|
|||||||
'<div class="cli-section" id="tools-section"></div>' +
|
'<div class="cli-section" id="tools-section"></div>' +
|
||||||
'<div class="cli-section" id="ccw-section"></div>' +
|
'<div class="cli-section" id="ccw-section"></div>' +
|
||||||
'</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-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 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
|
// Render sub-panels
|
||||||
renderToolsSection();
|
renderToolsSection();
|
||||||
renderCcwSection();
|
renderCcwSection();
|
||||||
|
renderLanguageSettingsSection();
|
||||||
renderCliSettingsSection();
|
renderCliSettingsSection();
|
||||||
renderCcwEndpointToolsSection();
|
renderCcwEndpointToolsSection();
|
||||||
|
|
||||||
@@ -329,6 +329,11 @@ async function renderCliManager() {
|
|||||||
initStorageManager();
|
initStorageManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize index manager card
|
||||||
|
if (typeof initIndexManager === 'function') {
|
||||||
|
initIndexManager();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize Lucide icons
|
// Initialize Lucide icons
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
@@ -504,6 +509,94 @@ function renderCcwSection() {
|
|||||||
if (window.lucide) lucide.createIcons();
|
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) ==========
|
// ========== CLI Settings Section (Full Width) ==========
|
||||||
function renderCliSettingsSection() {
|
function renderCliSettingsSection() {
|
||||||
var container = document.getElementById('cli-settings-section');
|
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 graphData = { nodes: [], edges: [] };
|
||||||
var cyInstance = null;
|
var cyInstance = null;
|
||||||
var activeTab = 'graph';
|
var activeTab = 'graph';
|
||||||
|
var activeDataSource = 'code';
|
||||||
var nodeFilters = {
|
var nodeFilters = {
|
||||||
MODULE: true,
|
MODULE: true,
|
||||||
CLASS: 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() {
|
async function loadSearchProcessData() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/graph/search-process');
|
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> ' +
|
'<i data-lucide="arrow-right" class="w-3 h-3"></i> ' +
|
||||||
graphData.edges.length + ' ' + t('graph.edges') +
|
graphData.edges.length + ' ' + t('graph.edges') +
|
||||||
'</span>' +
|
'</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>' +
|
||||||
'<div class="graph-toolbar-right">' +
|
'<div class="graph-toolbar-right">' +
|
||||||
'<button class="btn-icon" onclick="fitCytoscape()" title="' + t('graph.fitView') + '">' +
|
'<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 ==========
|
// ========== Data Refresh ==========
|
||||||
async function refreshGraphData() {
|
async function refreshGraphData() {
|
||||||
if (window.showToast) {
|
if (window.showToast) {
|
||||||
showToast(t('common.refreshing'), 'info');
|
showToast(t('common.refreshing'), 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeDataSource === 'memory') {
|
||||||
|
await loadCoreMemoryGraphData();
|
||||||
|
} else {
|
||||||
await loadGraphData();
|
await loadGraphData();
|
||||||
|
}
|
||||||
|
|
||||||
if (activeTab === 'graph' && cyInstance) {
|
if (activeTab === 'graph' && cyInstance) {
|
||||||
refreshCytoscape();
|
refreshCytoscape();
|
||||||
|
|||||||
@@ -424,6 +424,10 @@
|
|||||||
<i data-lucide="database" class="nav-icon"></i>
|
<i data-lucide="database" class="nav-icon"></i>
|
||||||
<span class="nav-text flex-1" data-i18n="nav.contextMemory">Context</span>
|
<span class="nav-text flex-1" data-i18n="nav.contextMemory">Context</span>
|
||||||
</li>
|
</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">
|
<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>
|
<i data-lucide="message-square" class="nav-icon"></i>
|
||||||
<span class="nav-text flex-1" data-i18n="nav.promptHistory">Prompts</span>
|
<span class="nav-text flex-1" data-i18n="nav.promptHistory">Prompts</span>
|
||||||
@@ -537,11 +541,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Main Content Container -->
|
||||||
<section class="main-content" id="mainContent">
|
<section class="main-content" id="mainContent">
|
||||||
<!-- Dynamic content: sessions grid or session detail page -->
|
<!-- 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';
|
import { executeInitWithProgress } from './smart-search.js';
|
||||||
// codex_lens removed - functionality integrated into smart_search
|
// codex_lens removed - functionality integrated into smart_search
|
||||||
import * as readFileMod from './read-file.js';
|
import * as readFileMod from './read-file.js';
|
||||||
|
import * as coreMemoryMod from './core-memory.js';
|
||||||
import type { ProgressInfo } from './codex-lens.js';
|
import type { ProgressInfo } from './codex-lens.js';
|
||||||
|
|
||||||
// Import legacy JS tools
|
// Import legacy JS tools
|
||||||
@@ -355,6 +356,7 @@ registerTool(toLegacyTool(cliExecutorMod));
|
|||||||
registerTool(toLegacyTool(smartSearchMod));
|
registerTool(toLegacyTool(smartSearchMod));
|
||||||
// codex_lens removed - functionality integrated into smart_search
|
// codex_lens removed - functionality integrated into smart_search
|
||||||
registerTool(toLegacyTool(readFileMod));
|
registerTool(toLegacyTool(readFileMod));
|
||||||
|
registerTool(toLegacyTool(coreMemoryMod));
|
||||||
|
|
||||||
// Register legacy JS tools
|
// Register legacy JS tools
|
||||||
registerTool(uiGeneratePreviewTool);
|
registerTool(uiGeneratePreviewTool);
|
||||||
|
|||||||
Reference in New Issue
Block a user