@grec0/memory-bank-mcp 0.0.2

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.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * @fileoverview Index manager for Memory Bank
3
+ * Coordinates scanning, chunking, embedding, and storage
4
+ */
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { scanFiles, scanSingleFile } from "./fileScanner.js";
8
+ import { chunkCode } from "./chunker.js";
9
+ /**
10
+ * Index manager coordinating the entire indexing pipeline
11
+ */
12
+ export class IndexManager {
13
+ embeddingService;
14
+ vectorStore;
15
+ metadataPath;
16
+ metadata;
17
+ constructor(embeddingService, vectorStore, storagePath = ".memorybank") {
18
+ this.embeddingService = embeddingService;
19
+ this.vectorStore = vectorStore;
20
+ this.metadataPath = path.join(storagePath, "index-metadata.json");
21
+ this.metadata = this.loadMetadata();
22
+ }
23
+ /**
24
+ * Loads index metadata from disk
25
+ */
26
+ loadMetadata() {
27
+ try {
28
+ if (fs.existsSync(this.metadataPath)) {
29
+ const data = fs.readFileSync(this.metadataPath, "utf-8");
30
+ return JSON.parse(data);
31
+ }
32
+ }
33
+ catch (error) {
34
+ console.error(`Warning: Could not load index metadata: ${error}`);
35
+ }
36
+ return {
37
+ version: "1.0",
38
+ lastIndexed: 0,
39
+ files: {},
40
+ };
41
+ }
42
+ /**
43
+ * Saves index metadata to disk
44
+ */
45
+ saveMetadata() {
46
+ try {
47
+ const dir = path.dirname(this.metadataPath);
48
+ if (!fs.existsSync(dir)) {
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ }
51
+ fs.writeFileSync(this.metadataPath, JSON.stringify(this.metadata, null, 2));
52
+ }
53
+ catch (error) {
54
+ console.error(`Warning: Could not save index metadata: ${error}`);
55
+ }
56
+ }
57
+ /**
58
+ * Checks if a file needs reindexing
59
+ */
60
+ needsReindexing(file, forceReindex) {
61
+ if (forceReindex) {
62
+ return true;
63
+ }
64
+ const fileInfo = this.metadata.files[file.path];
65
+ if (!fileInfo) {
66
+ return true; // New file
67
+ }
68
+ if (fileInfo.hash !== file.hash) {
69
+ return true; // File changed
70
+ }
71
+ return false;
72
+ }
73
+ /**
74
+ * Indexes a single file
75
+ */
76
+ async indexFile(file, forceReindex = false) {
77
+ try {
78
+ // Check if file needs reindexing
79
+ if (!this.needsReindexing(file, forceReindex)) {
80
+ console.error(`Skipping ${file.path} (no changes)`);
81
+ return { chunksCreated: 0 };
82
+ }
83
+ console.error(`Indexing: ${file.path}`);
84
+ // Read file content
85
+ const content = fs.readFileSync(file.absolutePath, "utf-8");
86
+ // Get chunk size from environment or use defaults
87
+ const maxChunkSize = parseInt(process.env.MEMORYBANK_CHUNK_SIZE || "1000");
88
+ const chunkOverlap = parseInt(process.env.MEMORYBANK_CHUNK_OVERLAP || "200");
89
+ // Chunk the code
90
+ const chunks = chunkCode({
91
+ filePath: file.path,
92
+ content,
93
+ language: file.language,
94
+ maxChunkSize,
95
+ chunkOverlap,
96
+ });
97
+ if (chunks.length === 0) {
98
+ console.error(`Warning: No chunks created for ${file.path}`);
99
+ return { chunksCreated: 0 };
100
+ }
101
+ console.error(` Created ${chunks.length} chunks`);
102
+ // Generate embeddings
103
+ const embeddingInputs = chunks.map((chunk) => ({
104
+ id: chunk.id,
105
+ content: chunk.content,
106
+ }));
107
+ const embeddings = await this.embeddingService.generateBatchEmbeddings(embeddingInputs);
108
+ console.error(` Generated ${embeddings.length} embeddings`);
109
+ // Prepare chunk records for storage
110
+ const timestamp = Date.now();
111
+ const chunkRecords = chunks.map((chunk, i) => ({
112
+ id: chunk.id,
113
+ vector: embeddings[i].vector,
114
+ filePath: chunk.filePath,
115
+ content: chunk.content,
116
+ startLine: chunk.startLine,
117
+ endLine: chunk.endLine,
118
+ chunkType: chunk.chunkType,
119
+ name: chunk.name,
120
+ language: chunk.language,
121
+ fileHash: file.hash,
122
+ timestamp,
123
+ context: chunk.context,
124
+ }));
125
+ // Delete old chunks for this file
126
+ await this.vectorStore.deleteChunksByFile(file.path);
127
+ // Insert new chunks
128
+ await this.vectorStore.insertChunks(chunkRecords);
129
+ console.error(` Stored ${chunkRecords.length} chunks in vector store`);
130
+ // Update metadata
131
+ this.metadata.files[file.path] = {
132
+ hash: file.hash,
133
+ lastIndexed: timestamp,
134
+ chunkCount: chunks.length,
135
+ };
136
+ this.metadata.lastIndexed = timestamp;
137
+ this.saveMetadata();
138
+ return { chunksCreated: chunks.length };
139
+ }
140
+ catch (error) {
141
+ const errorMsg = `Error indexing ${file.path}: ${error}`;
142
+ console.error(errorMsg);
143
+ return { chunksCreated: 0, error: errorMsg };
144
+ }
145
+ }
146
+ /**
147
+ * Indexes multiple files or a directory
148
+ */
149
+ async indexFiles(options) {
150
+ const startTime = Date.now();
151
+ console.error(`\n=== Starting indexing process ===`);
152
+ console.error(`Root path: ${options.rootPath}`);
153
+ console.error(`Force reindex: ${options.forceReindex || false}`);
154
+ // Initialize vector store
155
+ await this.vectorStore.initialize();
156
+ // Scan files
157
+ console.error(`\nScanning files...`);
158
+ const files = scanFiles({
159
+ rootPath: options.rootPath,
160
+ recursive: options.recursive !== undefined ? options.recursive : true,
161
+ });
162
+ if (files.length === 0) {
163
+ console.error("No files found to index");
164
+ return {
165
+ filesProcessed: 0,
166
+ chunksCreated: 0,
167
+ errors: [],
168
+ duration: Date.now() - startTime,
169
+ };
170
+ }
171
+ // Filter files that need reindexing
172
+ const filesToIndex = files.filter((file) => this.needsReindexing(file, options.forceReindex || false));
173
+ console.error(`\nFound ${files.length} files, ${filesToIndex.length} need indexing`);
174
+ if (filesToIndex.length === 0) {
175
+ console.error("All files are up to date");
176
+ return {
177
+ filesProcessed: 0,
178
+ chunksCreated: 0,
179
+ errors: [],
180
+ duration: Date.now() - startTime,
181
+ };
182
+ }
183
+ // Index files
184
+ const errors = [];
185
+ let totalChunks = 0;
186
+ let processedFiles = 0;
187
+ for (let i = 0; i < filesToIndex.length; i++) {
188
+ const file = filesToIndex[i];
189
+ console.error(`\n[${i + 1}/${filesToIndex.length}] Processing ${file.path}`);
190
+ const result = await this.indexFile(file, options.forceReindex || false);
191
+ if (result.error) {
192
+ errors.push(result.error);
193
+ }
194
+ else {
195
+ processedFiles++;
196
+ totalChunks += result.chunksCreated;
197
+ }
198
+ }
199
+ const duration = Date.now() - startTime;
200
+ console.error(`\n=== Indexing complete ===`);
201
+ console.error(`Files processed: ${processedFiles}`);
202
+ console.error(`Chunks created: ${totalChunks}`);
203
+ console.error(`Errors: ${errors.length}`);
204
+ console.error(`Duration: ${(duration / 1000).toFixed(2)}s`);
205
+ return {
206
+ filesProcessed: processedFiles,
207
+ chunksCreated: totalChunks,
208
+ errors,
209
+ duration,
210
+ };
211
+ }
212
+ /**
213
+ * Re-indexes a specific file by path
214
+ */
215
+ async reindexFile(filePath, rootPath) {
216
+ try {
217
+ // Scan the specific file
218
+ const file = scanSingleFile(filePath, rootPath);
219
+ if (!file) {
220
+ return {
221
+ success: false,
222
+ chunksCreated: 0,
223
+ error: "File not found or not a code file",
224
+ };
225
+ }
226
+ // Initialize vector store
227
+ await this.vectorStore.initialize();
228
+ // Index the file
229
+ const result = await this.indexFile(file, true);
230
+ if (result.error) {
231
+ return {
232
+ success: false,
233
+ chunksCreated: 0,
234
+ error: result.error,
235
+ };
236
+ }
237
+ return {
238
+ success: true,
239
+ chunksCreated: result.chunksCreated,
240
+ };
241
+ }
242
+ catch (error) {
243
+ return {
244
+ success: false,
245
+ chunksCreated: 0,
246
+ error: `Error reindexing file: ${error}`,
247
+ };
248
+ }
249
+ }
250
+ /**
251
+ * Gets statistics about the index
252
+ */
253
+ async getStats() {
254
+ await this.vectorStore.initialize();
255
+ const vectorStats = await this.vectorStore.getStats();
256
+ const fileHashes = await this.vectorStore.getFileHashes();
257
+ // Check for files that need reindexing
258
+ const pendingFiles = [];
259
+ for (const [filePath, storedHash] of fileHashes) {
260
+ const metadataHash = this.metadata.files[filePath]?.hash;
261
+ if (metadataHash && metadataHash !== storedHash) {
262
+ pendingFiles.push(filePath);
263
+ }
264
+ }
265
+ return {
266
+ totalFiles: vectorStats.fileCount,
267
+ totalChunks: vectorStats.totalChunks,
268
+ lastIndexed: vectorStats.lastUpdated,
269
+ languages: vectorStats.languageCounts,
270
+ pendingFiles: pendingFiles.length > 0 ? pendingFiles : undefined,
271
+ };
272
+ }
273
+ /**
274
+ * Searches the index
275
+ */
276
+ async search(query, options = {}) {
277
+ await this.vectorStore.initialize();
278
+ // Generate query embedding
279
+ const queryVector = await this.embeddingService.generateQueryEmbedding(query);
280
+ // Search vector store
281
+ const results = await this.vectorStore.search(queryVector, {
282
+ topK: options.topK || 10,
283
+ minScore: options.minScore || 0.0,
284
+ filterByFile: options.filterByFile,
285
+ filterByLanguage: options.filterByLanguage,
286
+ });
287
+ // Format results
288
+ return results.map((result) => ({
289
+ filePath: result.chunk.filePath,
290
+ content: result.chunk.content,
291
+ startLine: result.chunk.startLine,
292
+ endLine: result.chunk.endLine,
293
+ chunkType: result.chunk.chunkType,
294
+ name: result.chunk.name,
295
+ language: result.chunk.language,
296
+ score: result.score,
297
+ }));
298
+ }
299
+ /**
300
+ * Clears the entire index
301
+ */
302
+ async clearIndex() {
303
+ await this.vectorStore.initialize();
304
+ await this.vectorStore.clear();
305
+ this.metadata = {
306
+ version: "1.0",
307
+ lastIndexed: 0,
308
+ files: {},
309
+ };
310
+ this.saveMetadata();
311
+ // Clear embedding cache
312
+ this.embeddingService.clearCache();
313
+ console.error("Index cleared");
314
+ }
315
+ /**
316
+ * Removes a file from the index
317
+ */
318
+ async removeFile(filePath) {
319
+ await this.vectorStore.initialize();
320
+ await this.vectorStore.deleteChunksByFile(filePath);
321
+ delete this.metadata.files[filePath];
322
+ this.saveMetadata();
323
+ console.error(`Removed ${filePath} from index`);
324
+ }
325
+ }
326
+ /**
327
+ * Creates an index manager from environment variables
328
+ */
329
+ export function createIndexManager(embeddingService, vectorStore) {
330
+ const storagePath = process.env.MEMORYBANK_STORAGE_PATH || ".memorybank";
331
+ return new IndexManager(embeddingService, vectorStore, storagePath);
332
+ }
@@ -0,0 +1,49 @@
1
+ // Global variables to store user IDs
2
+ let adminUserId = null;
3
+ import { getUserIdByEmail, getUserIdByUsername } from "./utils.js";
4
+ /**
5
+ * Gets the admin user ID by looking up the user by email or username
6
+ *
7
+ * This function will try the following methods in order:
8
+ * 1. Use the cached admin user ID if available
9
+ * 2. Use the PLANKA_ADMIN_ID environment variable if set (for backwards compatibility)
10
+ * 3. Look up the admin user ID by email using PLANKA_ADMIN_EMAIL
11
+ * 4. Look up the admin user ID by username using PLANKA_ADMIN_USERNAME
12
+ */
13
+ export async function getAdminUserId() {
14
+ if (adminUserId) {
15
+ return adminUserId;
16
+ }
17
+ try {
18
+ // Check for direct admin ID (for backwards compatibility)
19
+ const directAdminId = process.env.PLANKA_ADMIN_ID;
20
+ if (directAdminId) {
21
+ adminUserId = directAdminId;
22
+ return adminUserId;
23
+ }
24
+ // Try to get the admin ID by email
25
+ const adminEmail = process.env.PLANKA_ADMIN_EMAIL;
26
+ if (adminEmail) {
27
+ const id = await getUserIdByEmail(adminEmail);
28
+ if (id) {
29
+ adminUserId = id;
30
+ return adminUserId;
31
+ }
32
+ }
33
+ // If that fails, try to get the admin ID by username
34
+ const adminUsername = process.env.PLANKA_ADMIN_USERNAME;
35
+ if (adminUsername) {
36
+ const id = await getUserIdByUsername(adminUsername);
37
+ if (id) {
38
+ adminUserId = id;
39
+ return adminUserId;
40
+ }
41
+ }
42
+ console.error("Could not determine admin user ID. Please set PLANKA_ADMIN_ID, PLANKA_ADMIN_EMAIL, or PLANKA_ADMIN_USERNAME.");
43
+ return null;
44
+ }
45
+ catch (error) {
46
+ console.error("Failed to get admin user ID:", error);
47
+ return null;
48
+ }
49
+ }
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ // Planka schemas
3
+ export const PlankaUserSchema = z.object({
4
+ id: z.string(),
5
+ email: z.string(),
6
+ name: z.string().nullable(),
7
+ username: z.string(),
8
+ avatarUrl: z.string().nullable(),
9
+ createdAt: z.string(),
10
+ updatedAt: z.string().nullable(),
11
+ });
12
+ export const PlankaProjectSchema = z.object({
13
+ id: z.string(),
14
+ name: z.string(),
15
+ background: z.string().nullable(),
16
+ createdAt: z.string(),
17
+ updatedAt: z.string().nullable(),
18
+ });
19
+ export const PlankaBoardSchema = z.object({
20
+ id: z.string(),
21
+ projectId: z.string(),
22
+ name: z.string(),
23
+ position: z.number(),
24
+ createdAt: z.string(),
25
+ updatedAt: z.string().nullable(),
26
+ });
27
+ export const PlankaListSchema = z.object({
28
+ id: z.string(),
29
+ boardId: z.string(),
30
+ name: z.string(),
31
+ position: z.number(),
32
+ createdAt: z.string(),
33
+ updatedAt: z.string().nullable(),
34
+ });
35
+ export const PlankaLabelSchema = z.object({
36
+ id: z.string(),
37
+ boardId: z.string(),
38
+ name: z.string(),
39
+ color: z.string(),
40
+ createdAt: z.string(),
41
+ updatedAt: z.string().nullable(),
42
+ });
43
+ // Define the stopwatch schema
44
+ export const PlankaStopwatchSchema = z.object({
45
+ startedAt: z.string().nullable(),
46
+ total: z.number(),
47
+ });
48
+ export const PlankaCardSchema = z.object({
49
+ id: z.string(),
50
+ listId: z.string(),
51
+ name: z.string(),
52
+ description: z.string().nullable(),
53
+ position: z.number(),
54
+ dueDate: z.string().nullable(),
55
+ isCompleted: z.boolean().optional(),
56
+ stopwatch: PlankaStopwatchSchema.nullable().optional(),
57
+ createdAt: z.string(),
58
+ updatedAt: z.string().nullable(),
59
+ });
60
+ export const PlankaTaskSchema = z.object({
61
+ id: z.string(),
62
+ cardId: z.string(),
63
+ name: z.string(),
64
+ isCompleted: z.boolean(),
65
+ position: z.number(),
66
+ createdAt: z.string(),
67
+ updatedAt: z.string().nullable(),
68
+ });
69
+ export const PlankaCommentSchema = z.object({
70
+ id: z.string(),
71
+ cardId: z.string(),
72
+ userId: z.string(),
73
+ text: z.string(),
74
+ createdAt: z.string(),
75
+ updatedAt: z.string().nullable(),
76
+ });
77
+ export const PlankaAttachmentSchema = z.object({
78
+ id: z.string(),
79
+ cardId: z.string(),
80
+ userId: z.string(),
81
+ name: z.string(),
82
+ url: z.string(),
83
+ createdAt: z.string(),
84
+ updatedAt: z.string().nullable(),
85
+ });
86
+ export const PlankaCardMembershipSchema = z.object({
87
+ id: z.string(),
88
+ cardId: z.string(),
89
+ userId: z.string(),
90
+ createdAt: z.string(),
91
+ updatedAt: z.string().nullable(),
92
+ });
93
+ export const PlankaBoardMembershipSchema = z.object({
94
+ id: z.string(),
95
+ boardId: z.string(),
96
+ userId: z.string(),
97
+ role: z.enum(["editor", "admin"]),
98
+ createdAt: z.string(),
99
+ updatedAt: z.string(),
100
+ });
101
+ export const PlankaProjectMembershipSchema = z.object({
102
+ id: z.string(),
103
+ projectId: z.string(),
104
+ userId: z.string(),
105
+ role: z.enum(["editor", "admin"]),
106
+ createdAt: z.string(),
107
+ updatedAt: z.string(),
108
+ });
109
+ export const PlankaCardLabelSchema = z.object({
110
+ id: z.string(),
111
+ cardId: z.string(),
112
+ labelId: z.string(),
113
+ createdAt: z.string(),
114
+ updatedAt: z.string(),
115
+ });