@mrxkun/mcfast-mcp 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -1
- package/package.json +3 -1
- package/src/index.js +191 -1
- package/src/memory/index.js +14 -0
- package/src/memory/memory-engine.js +530 -0
- package/src/memory/stores/database.js +104 -0
- package/src/memory/utils/chunker.js +94 -0
- package/src/memory/utils/daily-logs.js +263 -0
- package/src/memory/utils/dashboard-client.js +141 -0
- package/src/memory/utils/embedder.js +217 -0
- package/src/memory/utils/enhanced-embedder.js +717 -0
- package/src/memory/utils/indexer.js +118 -0
- package/src/memory/utils/simple-embedder.js +234 -0
- package/src/memory/utils/smart-router.js +344 -0
- package/src/memory/utils/sync-engine.js +373 -0
- package/src/memory/utils/ultra-embedder.js +1448 -0
- package/src/memory/watchers/file-watcher.js +61 -0
- package/src/tools/memory_get.js +235 -0
- package/src/tools/memory_search.js +296 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
|
|
3
|
+
// Simple debounce implementation to avoid lodash dependency
|
|
4
|
+
function debounce(func, wait) {
|
|
5
|
+
let timeout;
|
|
6
|
+
return function executedFunction(...args) {
|
|
7
|
+
const later = () => {
|
|
8
|
+
clearTimeout(timeout);
|
|
9
|
+
func(...args);
|
|
10
|
+
};
|
|
11
|
+
clearTimeout(timeout);
|
|
12
|
+
timeout = setTimeout(later, wait);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class FileWatcher {
|
|
17
|
+
constructor(projectPath, memoryEngine) {
|
|
18
|
+
this.projectPath = projectPath;
|
|
19
|
+
this.memory = memoryEngine;
|
|
20
|
+
this.watcher = null;
|
|
21
|
+
this.pendingUpdates = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async start() {
|
|
25
|
+
this.watcher = chokidar.watch(this.projectPath, {
|
|
26
|
+
ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
|
|
27
|
+
persistent: true
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.watcher.on('add', path => this.queueUpdate(path, 'add'));
|
|
31
|
+
this.watcher.on('change', path => this.queueUpdate(path, 'change'));
|
|
32
|
+
this.watcher.on('unlink', path => this.handleDelete(path));
|
|
33
|
+
|
|
34
|
+
this.processQueue = debounce(() => this.flushQueue(), 1500);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
queueUpdate(filePath, type) {
|
|
38
|
+
this.pendingUpdates.set(filePath, { path: filePath, type, timestamp: Date.now() });
|
|
39
|
+
this.processQueue();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async flushQueue() {
|
|
43
|
+
const updates = Array.from(this.pendingUpdates.values());
|
|
44
|
+
this.pendingUpdates.clear();
|
|
45
|
+
await Promise.all(updates.map(u => this.processUpdate(u)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async processUpdate(update) {
|
|
49
|
+
console.log(`[Watcher] ${update.type}: ${update.path}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async handleDelete(filePath) {
|
|
53
|
+
console.log(`[Watcher] Deleted: ${filePath}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async stop() {
|
|
57
|
+
if (this.watcher) await this.watcher.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default FileWatcher;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory_get Tool
|
|
3
|
+
* Read content from memory storage
|
|
4
|
+
* Version: 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute memory get
|
|
13
|
+
* @param {Object} args - Tool arguments
|
|
14
|
+
* @param {string} args.path - Path to memory file (relative to ~/.mcfast/memory/)
|
|
15
|
+
* @param {number} [args.from=1] - Start line
|
|
16
|
+
* @param {number} [args.lines=50] - Number of lines to read
|
|
17
|
+
* @param {Object} context - Runtime context
|
|
18
|
+
* @returns {Promise<Object>} File content
|
|
19
|
+
*/
|
|
20
|
+
async function execute(args, context = {}) {
|
|
21
|
+
const {
|
|
22
|
+
path: relativePath,
|
|
23
|
+
from = 1,
|
|
24
|
+
lines = 50
|
|
25
|
+
} = args;
|
|
26
|
+
|
|
27
|
+
if (!relativePath) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: "❌ Error: 'path' parameter is required"
|
|
32
|
+
}],
|
|
33
|
+
isError: true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Security: Only allow reading from memory directory
|
|
39
|
+
if (!relativePath.startsWith('memory/') && !relativePath.startsWith('/')) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: "❌ Error: Invalid path. Must be in memory/ directory"
|
|
44
|
+
}],
|
|
45
|
+
isError: true
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Construct full path
|
|
50
|
+
const memoryDir = path.join(os.homedir(), '.mcfast', 'memory');
|
|
51
|
+
const fullPath = path.join(memoryDir, relativePath.replace(/^\//, ''));
|
|
52
|
+
|
|
53
|
+
// Security: Ensure path is within memory directory
|
|
54
|
+
if (!fullPath.startsWith(memoryDir)) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: "❌ Error: Path traversal not allowed"
|
|
59
|
+
}],
|
|
60
|
+
isError: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if file exists
|
|
65
|
+
if (!fs.existsSync(fullPath)) {
|
|
66
|
+
// Check if it's a special memory path
|
|
67
|
+
if (relativePath === 'memory/logs' || relativePath === 'logs') {
|
|
68
|
+
return await getMemoryLogs(memoryDir, from, lines);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (relativePath === 'memory/stats' || relativePath === 'stats') {
|
|
72
|
+
return await getMemoryStats(context.store);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
content: [{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `❌ File not found: ${relativePath}`
|
|
79
|
+
}],
|
|
80
|
+
isError: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const stats = fs.statSync(fullPath);
|
|
85
|
+
if (!stats.isFile()) {
|
|
86
|
+
return {
|
|
87
|
+
content: [{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: "❌ Path is not a file"
|
|
90
|
+
}],
|
|
91
|
+
isError: true
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Read file content
|
|
96
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
97
|
+
const allLines = content.split('\n');
|
|
98
|
+
|
|
99
|
+
// Extract specific lines
|
|
100
|
+
const startLine = Math.max(1, from);
|
|
101
|
+
const endLine = Math.min(allLines.length, from - 1 + lines);
|
|
102
|
+
const extractedLines = allLines.slice(startLine - 1, endLine);
|
|
103
|
+
const extractedContent = extractedLines.join('\n');
|
|
104
|
+
|
|
105
|
+
// Format output
|
|
106
|
+
const output = `📄 Memory: ${relativePath}\n`;
|
|
107
|
+
output += `Lines: ${startLine}-${endLine} of ${allLines.length}\n`;
|
|
108
|
+
output += '─'.repeat(40) + '\n';
|
|
109
|
+
output += extractedContent;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: output
|
|
115
|
+
}],
|
|
116
|
+
metadata: {
|
|
117
|
+
path: relativePath,
|
|
118
|
+
lines: extractedLines.length,
|
|
119
|
+
totalLines: allLines.length
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('[memory_get] Error:', error);
|
|
124
|
+
return {
|
|
125
|
+
content: [{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `❌ Error reading memory: ${error.message}`
|
|
128
|
+
}],
|
|
129
|
+
isError: true
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get memory logs (daily logs)
|
|
136
|
+
*/
|
|
137
|
+
async function getMemoryLogs(memoryDir, from, lines) {
|
|
138
|
+
const logsDir = path.join(memoryDir, 'logs');
|
|
139
|
+
|
|
140
|
+
if (!fs.existsSync(logsDir)) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: "📝 No memory logs found"
|
|
145
|
+
}],
|
|
146
|
+
metadata: { empty: true }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Get all log files
|
|
151
|
+
const logFiles = fs.readdirSync(logsDir)
|
|
152
|
+
.filter(f => f.endsWith('.md'))
|
|
153
|
+
.sort()
|
|
154
|
+
.reverse();
|
|
155
|
+
|
|
156
|
+
if (logFiles.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: "📝 No memory logs found"
|
|
161
|
+
}],
|
|
162
|
+
metadata: { empty: true }
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Read latest logs
|
|
167
|
+
let output = `📝 Memory Logs\n`;
|
|
168
|
+
output += `Found: ${logFiles.length} log file(s)\n`;
|
|
169
|
+
output += '─'.repeat(40) + '\n\n';
|
|
170
|
+
|
|
171
|
+
const filesToRead = logFiles.slice(0, 5);
|
|
172
|
+
for (const file of filesToRead) {
|
|
173
|
+
const filePath = path.join(logsDir, file);
|
|
174
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
175
|
+
const lines = content.split('\n').slice(0, 20);
|
|
176
|
+
|
|
177
|
+
output += `📅 ${file.replace('.md', '')}\n`;
|
|
178
|
+
output += lines.join('\n');
|
|
179
|
+
output += '\n\n---\n\n';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
content: [{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: output
|
|
186
|
+
}]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get memory statistics
|
|
192
|
+
*/
|
|
193
|
+
async function getMemoryStats(store) {
|
|
194
|
+
if (!store) {
|
|
195
|
+
return {
|
|
196
|
+
content: [{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: "❌ Memory store not initialized"
|
|
199
|
+
}],
|
|
200
|
+
isError: true
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const stats = store.getStats();
|
|
206
|
+
const syncState = store.getSyncState();
|
|
207
|
+
|
|
208
|
+
let output = `📊 Memory Statistics\n`;
|
|
209
|
+
output += '─'.repeat(40) + '\n';
|
|
210
|
+
output += `Indexed Files: ${stats.fileCount}\n`;
|
|
211
|
+
output += `Facts Stored: ${stats.factCount}\n`;
|
|
212
|
+
output += `Code Chunks: ${stats.chunkCount}\n`;
|
|
213
|
+
output += `Edit History: ${stats.editCount}\n\n`;
|
|
214
|
+
output += `Last Sync: ${syncState?.last_sync_time ? new Date(syncState.last_sync_time).toISOString() : 'Never'}\n`;
|
|
215
|
+
output += `Sync Status: ${syncState?.last_sync_status || 'unknown'}\n`;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
content: [{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: output
|
|
221
|
+
}],
|
|
222
|
+
metadata: stats
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return {
|
|
226
|
+
content: [{
|
|
227
|
+
type: "text",
|
|
228
|
+
text: `❌ Error getting stats: ${error.message}`
|
|
229
|
+
}],
|
|
230
|
+
isError: true
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { execute };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory_search Tool
|
|
3
|
+
* Hybrid search tool combining vector and keyword search
|
|
4
|
+
* Version: 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { LocalMemoryStore } = require('./local-store');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute memory search
|
|
11
|
+
* @param {Object} args - Tool arguments
|
|
12
|
+
* @param {string} args.query - Search query
|
|
13
|
+
* @param {string} [args.type='all'] - Filter by type: 'all', 'facts', 'history', 'code'
|
|
14
|
+
* @param {number} [args.maxResults=6] - Maximum results to return
|
|
15
|
+
* @param {number} [args.minScore=0.35] - Minimum score threshold
|
|
16
|
+
* @param {Object} [args.context] - Context information
|
|
17
|
+
* @param {string} [args.context.currentFile] - Current file being edited
|
|
18
|
+
* @param {number} [args.context.currentLine] - Current line number
|
|
19
|
+
* @param {Object} context - Runtime context
|
|
20
|
+
* @returns {Promise<Object>} Search results
|
|
21
|
+
*/
|
|
22
|
+
async function execute(args, context = {}) {
|
|
23
|
+
const {
|
|
24
|
+
query,
|
|
25
|
+
type = 'all',
|
|
26
|
+
maxResults = 6,
|
|
27
|
+
minScore = 0.35,
|
|
28
|
+
context: contextInfo = {}
|
|
29
|
+
} = args;
|
|
30
|
+
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Initialize store if not provided
|
|
35
|
+
let store = context.store;
|
|
36
|
+
if (!store) {
|
|
37
|
+
store = new LocalMemoryStore();
|
|
38
|
+
await store.initialize();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get embedding for query if possible
|
|
42
|
+
let embedding = null;
|
|
43
|
+
try {
|
|
44
|
+
embedding = await getEmbedding(query);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.log('[memory_search] Embedding not available, using keyword search only');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Perform hybrid search
|
|
50
|
+
let results;
|
|
51
|
+
|
|
52
|
+
if (type === 'facts') {
|
|
53
|
+
// Search facts table only
|
|
54
|
+
results = await searchFacts(store, query, { maxResults });
|
|
55
|
+
} else if (type === 'history') {
|
|
56
|
+
// Search edit history
|
|
57
|
+
results = await searchHistory(store, query, { maxResults });
|
|
58
|
+
} else if (type === 'code') {
|
|
59
|
+
// Search code chunks only
|
|
60
|
+
results = store.hybridSearch(query, embedding, {
|
|
61
|
+
vectorWeight: 0.7,
|
|
62
|
+
keywordWeight: 0.3,
|
|
63
|
+
maxResults,
|
|
64
|
+
minScore
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
// Search all (hybrid)
|
|
68
|
+
results = await searchAll(store, query, embedding, { maxResults, minScore });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Format output
|
|
72
|
+
const formattedResults = formatResults(results, type);
|
|
73
|
+
|
|
74
|
+
const latency = Date.now() - startTime;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: formattedResults
|
|
80
|
+
}],
|
|
81
|
+
metadata: {
|
|
82
|
+
query,
|
|
83
|
+
type,
|
|
84
|
+
resultCount: results.length,
|
|
85
|
+
latency,
|
|
86
|
+
sources: [...new Set(results.map(r => r.source || 'unknown'))]
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('[memory_search] Error:', error);
|
|
91
|
+
return {
|
|
92
|
+
content: [{
|
|
93
|
+
type: "text",
|
|
94
|
+
text: `❌ Memory search error: ${error.message}`
|
|
95
|
+
}],
|
|
96
|
+
isError: true
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Search facts only
|
|
103
|
+
*/
|
|
104
|
+
async function searchFacts(store, query, options) {
|
|
105
|
+
const { maxResults = 10 } = options;
|
|
106
|
+
|
|
107
|
+
const facts = store.searchFacts(query, { maxResults });
|
|
108
|
+
|
|
109
|
+
return facts.map(fact => ({
|
|
110
|
+
type: 'fact',
|
|
111
|
+
content: `${fact.type}: ${fact.name}${fact.signature ? ` (${fact.signature})' : ''}`,
|
|
112
|
+
file: fact.file_path,
|
|
113
|
+
lines: [fact.line_start, fact.line_end],
|
|
114
|
+
score: fact.confidence || 0.8,
|
|
115
|
+
source: 'facts'
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Search edit history
|
|
121
|
+
*/
|
|
122
|
+
async function searchHistory(store, query, options) {
|
|
123
|
+
const { maxResults = 10 } = options;
|
|
124
|
+
|
|
125
|
+
const history = store.getEditHistory({ limit: maxResults * 2 });
|
|
126
|
+
|
|
127
|
+
// Filter by query relevance
|
|
128
|
+
const filtered = history.filter(edit =>
|
|
129
|
+
edit.instruction.toLowerCase().includes(query.toLowerCase()) ||
|
|
130
|
+
edit.files.some(f => f.toLowerCase().includes(query.toLowerCase()))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return filtered.slice(0, maxResults).map(edit => ({
|
|
134
|
+
type: 'history',
|
|
135
|
+
content: edit.instruction,
|
|
136
|
+
files: edit.files,
|
|
137
|
+
strategy: edit.strategy,
|
|
138
|
+
success: edit.success,
|
|
139
|
+
timestamp: edit.timestamp,
|
|
140
|
+
score: edit.success ? 0.8 : 0.5,
|
|
141
|
+
source: 'history'
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Search all sources (hybrid)
|
|
147
|
+
*/
|
|
148
|
+
async function searchAll(store, query, embedding, options) {
|
|
149
|
+
const { maxResults = 10, minScore = 0.35 } = options;
|
|
150
|
+
|
|
151
|
+
const results = [];
|
|
152
|
+
|
|
153
|
+
// Get facts
|
|
154
|
+
const facts = await searchFacts(store, query, { maxResults: maxResults * 2 });
|
|
155
|
+
results.push(...facts);
|
|
156
|
+
|
|
157
|
+
// Get code chunks via hybrid search
|
|
158
|
+
if (embedding) {
|
|
159
|
+
const chunks = store.hybridSearch(query, embedding, {
|
|
160
|
+
vectorWeight: 0.7,
|
|
161
|
+
keywordWeight: 0.3,
|
|
162
|
+
maxResults: maxResults * 2,
|
|
163
|
+
minScore: minScore * 0.5 // Lower threshold for chunks
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
results.push(...chunks.map(chunk => ({
|
|
167
|
+
type: 'chunk',
|
|
168
|
+
content: chunk.content,
|
|
169
|
+
file: chunk.filePath,
|
|
170
|
+
lines: [chunk.startLine, chunk.endLine],
|
|
171
|
+
score: chunk.weightedScore || chunk.score,
|
|
172
|
+
source: chunk.source
|
|
173
|
+
})));
|
|
174
|
+
} else {
|
|
175
|
+
// Fallback to keyword search only
|
|
176
|
+
const keywordResults = store.ftsSearch(query, { maxResults: maxResults * 2 });
|
|
177
|
+
results.push(...keywordResults.map(r => ({
|
|
178
|
+
type: 'chunk',
|
|
179
|
+
content: r.content,
|
|
180
|
+
file: r.file_path,
|
|
181
|
+
lines: [r.start_line, r.end_line],
|
|
182
|
+
score: 0.6,
|
|
183
|
+
source: 'keyword'
|
|
184
|
+
})));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Get recent history
|
|
188
|
+
const history = await searchHistory(store, query, { maxResults: Math.floor(maxResults / 2) });
|
|
189
|
+
results.push(...history);
|
|
190
|
+
|
|
191
|
+
// Deduplicate and sort by score
|
|
192
|
+
const uniqueMap = new Map();
|
|
193
|
+
for (const result of results) {
|
|
194
|
+
const key = `${result.type}:${result.file || ''}:${result.content?.substring(0, 50)}`;
|
|
195
|
+
if (!uniqueMap.has(key) || uniqueMap.get(key).score < result.score) {
|
|
196
|
+
uniqueMap.set(key, result);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return Array.from(uniqueMap.values())
|
|
201
|
+
.filter(r => r.score >= minScore)
|
|
202
|
+
.sort((a, b) => b.score - a.score)
|
|
203
|
+
.slice(0, maxResults);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Format results for display
|
|
208
|
+
*/
|
|
209
|
+
function formatResults(results, type) {
|
|
210
|
+
if (!results || results.length === 0) {
|
|
211
|
+
return `🔍 Memory Search: No results found`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let output = `🔍 Memory Search Results\n`;
|
|
215
|
+
output += `Found: ${results.length} results\n\n`;
|
|
216
|
+
|
|
217
|
+
// Group by type
|
|
218
|
+
const byType = {};
|
|
219
|
+
for (const result of results) {
|
|
220
|
+
if (!byType[result.type]) byType[result.type] = [];
|
|
221
|
+
byType[result.type].push(result);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const [resultType, items] of Object.entries(byType)) {
|
|
225
|
+
output += `${getTypeIcon(resultType)} ${resultType.toUpperCase()} (${items.length})\n`;
|
|
226
|
+
output += '─'.repeat(40) + '\n';
|
|
227
|
+
|
|
228
|
+
for (const item of items.slice(0, 5)) {
|
|
229
|
+
output += ` ${formatItem(item)}\n\n`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return output;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getTypeIcon(type) {
|
|
237
|
+
const icons = {
|
|
238
|
+
fact: '📌',
|
|
239
|
+
chunk: '📝',
|
|
240
|
+
history: '📜'
|
|
241
|
+
};
|
|
242
|
+
return icons[type] || '•';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function formatItem(item) {
|
|
246
|
+
let text = '';
|
|
247
|
+
|
|
248
|
+
if (item.file) {
|
|
249
|
+
text += `${item.file}`;
|
|
250
|
+
if (item.lines && item.lines[0]) {
|
|
251
|
+
text += `:${item.lines[0]}`;
|
|
252
|
+
if (item.lines[1] && item.lines[1] !== item.lines[0]) {
|
|
253
|
+
text += `-${item.lines[1]}`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
text += '\n';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Truncate content
|
|
260
|
+
const content = item.content?.substring(0, 150) || '';
|
|
261
|
+
text += ` ${content}${content.length >= 150 ? '...' : ''}`;
|
|
262
|
+
|
|
263
|
+
text += `\n Score: ${(item.score * 100).toFixed(0)}%`;
|
|
264
|
+
|
|
265
|
+
if (item.source) {
|
|
266
|
+
text += ` • ${item.source}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return text;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get embedding for query
|
|
274
|
+
* Placeholder - implement with actual embedding API
|
|
275
|
+
*/
|
|
276
|
+
async function getEmbedding(text) {
|
|
277
|
+
// Check if OpenAI is available
|
|
278
|
+
try {
|
|
279
|
+
const { OpenAI } = require('openai');
|
|
280
|
+
if (process.env.OPENAI_API_KEY) {
|
|
281
|
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
282
|
+
const response = await openai.embeddings.create({
|
|
283
|
+
model: 'text-embedding-3-small',
|
|
284
|
+
input: text
|
|
285
|
+
});
|
|
286
|
+
return new Float32Array(response.data[0].embedding);
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
// OpenAI not available
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Return null to fallback to keyword search
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = { execute };
|