memory-journal-mcp 3.0.0

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.
Files changed (107) hide show
  1. package/.dockerignore +88 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +76 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +89 -0
  5. package/.github/ISSUE_TEMPLATE/question.md +63 -0
  6. package/.github/dependabot.yml +110 -0
  7. package/.github/pull_request_template.md +110 -0
  8. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +346 -0
  9. package/.github/workflows/codeql.yml +45 -0
  10. package/.github/workflows/dependabot-auto-merge.yml +42 -0
  11. package/.github/workflows/docker-publish.yml +277 -0
  12. package/.github/workflows/lint-and-test.yml +58 -0
  13. package/.github/workflows/publish-npm.yml +75 -0
  14. package/.github/workflows/secrets-scanning.yml +32 -0
  15. package/.github/workflows/security-update.yml +99 -0
  16. package/.memory-journal-team.db +0 -0
  17. package/.trivyignore +18 -0
  18. package/CHANGELOG.md +19 -0
  19. package/CODE_OF_CONDUCT.md +128 -0
  20. package/CONTRIBUTING.md +209 -0
  21. package/DOCKER_README.md +377 -0
  22. package/Dockerfile +64 -0
  23. package/LICENSE +21 -0
  24. package/README.md +461 -0
  25. package/SECURITY.md +200 -0
  26. package/VERSION +1 -0
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +42 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/constants/ServerInstructions.d.ts +8 -0
  32. package/dist/constants/ServerInstructions.d.ts.map +1 -0
  33. package/dist/constants/ServerInstructions.js +26 -0
  34. package/dist/constants/ServerInstructions.js.map +1 -0
  35. package/dist/database/SqliteAdapter.d.ts +198 -0
  36. package/dist/database/SqliteAdapter.d.ts.map +1 -0
  37. package/dist/database/SqliteAdapter.js +736 -0
  38. package/dist/database/SqliteAdapter.js.map +1 -0
  39. package/dist/filtering/ToolFilter.d.ts +63 -0
  40. package/dist/filtering/ToolFilter.d.ts.map +1 -0
  41. package/dist/filtering/ToolFilter.js +242 -0
  42. package/dist/filtering/ToolFilter.js.map +1 -0
  43. package/dist/github/GitHubIntegration.d.ts +91 -0
  44. package/dist/github/GitHubIntegration.d.ts.map +1 -0
  45. package/dist/github/GitHubIntegration.js +317 -0
  46. package/dist/github/GitHubIntegration.js.map +1 -0
  47. package/dist/handlers/prompts/index.d.ts +28 -0
  48. package/dist/handlers/prompts/index.d.ts.map +1 -0
  49. package/dist/handlers/prompts/index.js +366 -0
  50. package/dist/handlers/prompts/index.js.map +1 -0
  51. package/dist/handlers/resources/index.d.ts +27 -0
  52. package/dist/handlers/resources/index.d.ts.map +1 -0
  53. package/dist/handlers/resources/index.js +453 -0
  54. package/dist/handlers/resources/index.js.map +1 -0
  55. package/dist/handlers/tools/index.d.ts +26 -0
  56. package/dist/handlers/tools/index.d.ts.map +1 -0
  57. package/dist/handlers/tools/index.js +982 -0
  58. package/dist/handlers/tools/index.js.map +1 -0
  59. package/dist/index.d.ts +11 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +13 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/server/McpServer.d.ts +18 -0
  64. package/dist/server/McpServer.d.ts.map +1 -0
  65. package/dist/server/McpServer.js +171 -0
  66. package/dist/server/McpServer.js.map +1 -0
  67. package/dist/types/index.d.ts +300 -0
  68. package/dist/types/index.d.ts.map +1 -0
  69. package/dist/types/index.js +15 -0
  70. package/dist/types/index.js.map +1 -0
  71. package/dist/utils/McpLogger.d.ts +61 -0
  72. package/dist/utils/McpLogger.d.ts.map +1 -0
  73. package/dist/utils/McpLogger.js +113 -0
  74. package/dist/utils/McpLogger.js.map +1 -0
  75. package/dist/utils/logger.d.ts +30 -0
  76. package/dist/utils/logger.d.ts.map +1 -0
  77. package/dist/utils/logger.js +70 -0
  78. package/dist/utils/logger.js.map +1 -0
  79. package/dist/vector/VectorSearchManager.d.ts +63 -0
  80. package/dist/vector/VectorSearchManager.d.ts.map +1 -0
  81. package/dist/vector/VectorSearchManager.js +235 -0
  82. package/dist/vector/VectorSearchManager.js.map +1 -0
  83. package/docker-compose.yml +37 -0
  84. package/eslint.config.js +86 -0
  85. package/mcp-config-example.json +21 -0
  86. package/package.json +71 -0
  87. package/releases/release-notes-v2.2.0.md +165 -0
  88. package/releases/release-notes.md +214 -0
  89. package/releases/v3.0.0.md +236 -0
  90. package/server.json +42 -0
  91. package/src/cli.ts +52 -0
  92. package/src/constants/ServerInstructions.ts +25 -0
  93. package/src/database/SqliteAdapter.ts +952 -0
  94. package/src/filtering/ToolFilter.ts +271 -0
  95. package/src/github/GitHubIntegration.ts +409 -0
  96. package/src/handlers/prompts/index.ts +420 -0
  97. package/src/handlers/resources/index.ts +529 -0
  98. package/src/handlers/tools/index.ts +1081 -0
  99. package/src/index.ts +53 -0
  100. package/src/server/McpServer.ts +230 -0
  101. package/src/types/index.ts +435 -0
  102. package/src/types/sql.js.d.ts +34 -0
  103. package/src/utils/McpLogger.ts +155 -0
  104. package/src/utils/logger.ts +98 -0
  105. package/src/vector/VectorSearchManager.ts +277 -0
  106. package/tools.json +300 -0
  107. package/tsconfig.json +51 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Type declarations for sql.js (WASM-based SQLite)
3
+ */
4
+
5
+ declare module 'sql.js' {
6
+ export type SqlValue = Record<string, unknown>;
7
+
8
+ export interface QueryExecResult {
9
+ columns: string[];
10
+ values: unknown[][];
11
+ }
12
+
13
+ export type ParamsObject = Record<string, unknown>;
14
+
15
+ export type BindParams = unknown[] | ParamsObject | null;
16
+
17
+ export interface Database {
18
+ run(sql: string, params?: BindParams): void;
19
+ exec(sql: string, params?: BindParams): QueryExecResult[];
20
+ each(sql: string, params: BindParams, callback: (row: Record<string, unknown>) => void): void;
21
+ export(): Uint8Array;
22
+ close(): void;
23
+ }
24
+
25
+ export interface SqlJsStatic {
26
+ Database: new (data?: ArrayLike<number>) => Database;
27
+ }
28
+
29
+ export interface InitSqlJsOptions {
30
+ locateFile?: (filename: string) => string;
31
+ }
32
+
33
+ export default function initSqlJs(options?: InitSqlJsOptions): Promise<SqlJsStatic>;
34
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Memory Journal MCP Server - MCP Protocol Logger
3
+ *
4
+ * Logging that sends structured messages via MCP notifications/message.
5
+ * Falls back to stderr when MCP server is not connected.
6
+ * Follows RFC 5424 severity levels as per MCP spec.
7
+ */
8
+
9
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+
11
+ /**
12
+ * MCP log levels per RFC 5424
13
+ */
14
+ export type McpLogLevel =
15
+ | 'debug'
16
+ | 'info'
17
+ | 'notice'
18
+ | 'warning'
19
+ | 'error'
20
+ | 'critical'
21
+ | 'alert'
22
+ | 'emergency';
23
+
24
+ const LOG_LEVEL_PRIORITY: Record<McpLogLevel, number> = {
25
+ emergency: 0,
26
+ alert: 1,
27
+ critical: 2,
28
+ error: 3,
29
+ warning: 4,
30
+ notice: 5,
31
+ info: 6,
32
+ debug: 7,
33
+ };
34
+
35
+ interface LogData {
36
+ message: string;
37
+ module?: string;
38
+ operation?: string;
39
+ [key: string]: unknown;
40
+ }
41
+
42
+ /**
43
+ * MCP Protocol Logger
44
+ *
45
+ * Sends structured log messages via MCP notifications/message protocol.
46
+ * Falls back to stderr when server not connected.
47
+ */
48
+ export class McpLogger {
49
+ private server: McpServer | null = null;
50
+ private minLevel: McpLogLevel = 'info';
51
+
52
+ /**
53
+ * Connect to MCP server for protocol logging
54
+ */
55
+ setServer(server: McpServer): void {
56
+ this.server = server;
57
+ }
58
+
59
+ /**
60
+ * Set minimum log level (from logging/setLevel request)
61
+ */
62
+ setLevel(level: McpLogLevel): void {
63
+ if (level in LOG_LEVEL_PRIORITY) {
64
+ this.minLevel = level;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get current minimum log level
70
+ */
71
+ getLevel(): McpLogLevel {
72
+ return this.minLevel;
73
+ }
74
+
75
+ /**
76
+ * Check if a level should be logged
77
+ */
78
+ private shouldLog(level: McpLogLevel): boolean {
79
+ return LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.minLevel];
80
+ }
81
+
82
+ /**
83
+ * Format message for stderr fallback
84
+ */
85
+ private formatForStderr(level: McpLogLevel, loggerName: string, data: LogData): string {
86
+ const timestamp = new Date().toISOString();
87
+ const levelUpper = level.toUpperCase().padEnd(9);
88
+ const module = loggerName ? `[${loggerName}]` : '';
89
+
90
+ let formatted = `[${timestamp}] [${levelUpper}] ${module} ${data.message}`;
91
+
92
+ // Add extra context (filter out message and module)
93
+ const extras = Object.fromEntries(
94
+ Object.entries(data).filter(([key]) => key !== 'message' && key !== 'module')
95
+ );
96
+
97
+ if (Object.keys(extras).length > 0) {
98
+ formatted += ` ${JSON.stringify(extras)}`;
99
+ }
100
+
101
+ return formatted;
102
+ }
103
+
104
+ /**
105
+ * Send log message via MCP protocol or fallback to stderr
106
+ */
107
+ log(level: McpLogLevel, loggerName: string, data: LogData): void {
108
+ if (!this.shouldLog(level)) return;
109
+
110
+ // Send via MCP protocol if server connected
111
+ if (this.server) {
112
+ try {
113
+ void this.server.sendLoggingMessage({
114
+ level,
115
+ logger: loggerName,
116
+ data,
117
+ });
118
+ } catch {
119
+ // Fallback to stderr if MCP send fails
120
+ console.error(this.formatForStderr(level, loggerName, data));
121
+ }
122
+ }
123
+
124
+ // Always also log to stderr for local debugging
125
+ console.error(this.formatForStderr(level, loggerName, data));
126
+ }
127
+
128
+ // Convenience methods
129
+ debug(loggerName: string, message: string, context?: Record<string, unknown>): void {
130
+ this.log('debug', loggerName, { message, ...context });
131
+ }
132
+
133
+ info(loggerName: string, message: string, context?: Record<string, unknown>): void {
134
+ this.log('info', loggerName, { message, ...context });
135
+ }
136
+
137
+ notice(loggerName: string, message: string, context?: Record<string, unknown>): void {
138
+ this.log('notice', loggerName, { message, ...context });
139
+ }
140
+
141
+ warning(loggerName: string, message: string, context?: Record<string, unknown>): void {
142
+ this.log('warning', loggerName, { message, ...context });
143
+ }
144
+
145
+ error(loggerName: string, message: string, context?: Record<string, unknown>): void {
146
+ this.log('error', loggerName, { message, ...context });
147
+ }
148
+
149
+ critical(loggerName: string, message: string, context?: Record<string, unknown>): void {
150
+ this.log('critical', loggerName, { message, ...context });
151
+ }
152
+ }
153
+
154
+ // Singleton instance
155
+ export const mcpLogger = new McpLogger();
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Memory Journal MCP Server - Logger
3
+ *
4
+ * Centralized logging to stderr only (stdout reserved for MCP protocol).
5
+ * Follows RFC 5424 severity levels.
6
+ */
7
+
8
+ type LogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical';
9
+
10
+ interface LogContext {
11
+ module?: string;
12
+ operation?: string;
13
+ entityId?: string | number;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ const LOG_LEVELS: Record<LogLevel, number> = {
18
+ debug: 7,
19
+ info: 6,
20
+ notice: 5,
21
+ warning: 4,
22
+ error: 3,
23
+ critical: 2,
24
+ };
25
+
26
+ class Logger {
27
+ private minLevel: number;
28
+
29
+ constructor(level: LogLevel = 'info') {
30
+ this.minLevel = LOG_LEVELS[level];
31
+ }
32
+
33
+ private shouldLog(level: LogLevel): boolean {
34
+ return LOG_LEVELS[level] <= this.minLevel;
35
+ }
36
+
37
+ private formatMessage(level: LogLevel, message: string, context?: LogContext): string {
38
+ const timestamp = new Date().toISOString();
39
+ const levelUpper = level.toUpperCase().padEnd(8);
40
+ const module = context?.module ? `[${context.module}]` : '';
41
+ const operation = context?.operation ? `[${context.operation}]` : '';
42
+
43
+ let formatted = `[${timestamp}] [${levelUpper}] ${module}${operation} ${message}`;
44
+
45
+ // Add context as JSON if there are additional fields
46
+ const extras = { ...context };
47
+ delete extras.module;
48
+ delete extras.operation;
49
+
50
+ if (Object.keys(extras).length > 0) {
51
+ formatted += ` ${JSON.stringify(extras)}`;
52
+ }
53
+
54
+ return formatted;
55
+ }
56
+
57
+ private log(level: LogLevel, message: string, context?: LogContext): void {
58
+ if (!this.shouldLog(level)) return;
59
+
60
+ const formatted = this.formatMessage(level, message, context);
61
+
62
+ // Always write to stderr (stdout is reserved for MCP protocol)
63
+ console.error(formatted);
64
+ }
65
+
66
+ debug(message: string, context?: LogContext): void {
67
+ this.log('debug', message, context);
68
+ }
69
+
70
+ info(message: string, context?: LogContext): void {
71
+ this.log('info', message, context);
72
+ }
73
+
74
+ notice(message: string, context?: LogContext): void {
75
+ this.log('notice', message, context);
76
+ }
77
+
78
+ warning(message: string, context?: LogContext): void {
79
+ this.log('warning', message, context);
80
+ }
81
+
82
+ error(message: string, context?: LogContext): void {
83
+ this.log('error', message, context);
84
+ }
85
+
86
+ critical(message: string, context?: LogContext): void {
87
+ this.log('critical', message, context);
88
+ }
89
+
90
+ setLevel(level: LogLevel): void {
91
+ this.minLevel = LOG_LEVELS[level];
92
+ }
93
+ }
94
+
95
+ // Get log level from environment
96
+ const envLevel = (process.env['LOG_LEVEL'] ?? 'info') as LogLevel;
97
+
98
+ export const logger = new Logger(envLevel);
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Memory Journal MCP Server - Vector Search Manager
3
+ *
4
+ * Semantic search using @xenova/transformers for embeddings
5
+ * and vectra for vector indexing.
6
+ */
7
+
8
+ import { pipeline } from '@xenova/transformers';
9
+ import { LocalIndex } from 'vectra';
10
+ import * as path from 'node:path';
11
+ import * as fs from 'node:fs';
12
+ import { logger } from '../utils/logger.js';
13
+ import type { SqliteAdapter } from '../database/SqliteAdapter.js';
14
+ import type { JournalEntry } from '../types/index.js';
15
+
16
+ /** Default model for embeddings (same as Python version) */
17
+ const DEFAULT_MODEL = 'Xenova/all-MiniLM-L6-v2';
18
+
19
+ /** Embedding dimensions for all-MiniLM-L6-v2 */
20
+ const EMBEDDING_DIMENSIONS = 384;
21
+
22
+ /** Search result with similarity score */
23
+ export interface SemanticSearchResult {
24
+ entryId: number;
25
+ score: number;
26
+ entry?: JournalEntry;
27
+ }
28
+
29
+ /**
30
+ * VectorSearchManager - Handles semantic search with local embeddings
31
+ */
32
+ export class VectorSearchManager {
33
+ // Use a more flexible type since FeatureExtractionPipeline doesn't fully implement Pipeline
34
+ private embedder: ((text: string, options?: Record<string, unknown>) => Promise<unknown>) | null = null;
35
+ private index: LocalIndex | null = null;
36
+ private readonly indexPath: string;
37
+ private readonly modelName: string;
38
+ private initialized = false;
39
+ private initializing = false;
40
+
41
+ constructor(dbPath: string, modelName = DEFAULT_MODEL) {
42
+ // Store index in same directory as database
43
+ const dbDir = path.dirname(dbPath);
44
+ this.indexPath = path.join(dbDir, '.vectra_index');
45
+ this.modelName = modelName;
46
+ }
47
+
48
+ /**
49
+ * Check if vector search is initialized
50
+ */
51
+ isInitialized(): boolean {
52
+ return this.initialized;
53
+ }
54
+
55
+ /**
56
+ * Initialize the vector search manager (lazy loading)
57
+ */
58
+ async initialize(): Promise<void> {
59
+ if (this.initialized || this.initializing) return;
60
+
61
+ this.initializing = true;
62
+
63
+ try {
64
+ logger.info('Initializing vector search...', { module: 'VectorSearch' });
65
+
66
+ // Load embedding model (downloads on first use, ~23MB)
67
+ logger.info(`Loading embedding model: ${this.modelName}`, { module: 'VectorSearch' });
68
+ this.embedder = await pipeline('feature-extraction', this.modelName, {
69
+ quantized: true, // Use quantized model for faster inference
70
+ });
71
+ logger.info('Embedding model loaded', { module: 'VectorSearch' });
72
+
73
+ // Create or load vectra index
74
+ if (!fs.existsSync(this.indexPath)) {
75
+ fs.mkdirSync(this.indexPath, { recursive: true });
76
+ }
77
+
78
+ this.index = new LocalIndex(this.indexPath);
79
+
80
+ // Check if index exists
81
+ if (!await this.index.isIndexCreated()) {
82
+ await this.index.createIndex();
83
+ logger.info('Created new vector index', { module: 'VectorSearch' });
84
+ } else {
85
+ logger.info('Loaded existing vector index', { module: 'VectorSearch' });
86
+ }
87
+
88
+ this.initialized = true;
89
+ this.initializing = false;
90
+ logger.info('Vector search initialized successfully', { module: 'VectorSearch' });
91
+ } catch (error) {
92
+ this.initializing = false;
93
+ logger.error('Failed to initialize vector search', {
94
+ module: 'VectorSearch',
95
+ error: error instanceof Error ? error.message : String(error)
96
+ });
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Generate embedding for text
103
+ */
104
+ async generateEmbedding(text: string): Promise<number[]> {
105
+ if (!this.embedder) {
106
+ throw new Error('Vector search not initialized');
107
+ }
108
+
109
+ // Generate embedding using feature-extraction pipeline
110
+ // The pipeline returns a Tensor with a data property containing the embeddings
111
+ const output = await this.embedder(text, {
112
+ pooling: 'mean',
113
+ normalize: true,
114
+ }) as { data: ArrayLike<number> };
115
+
116
+ // Convert to number array
117
+ const embedding = Array.from(output.data);
118
+ return embedding;
119
+ }
120
+
121
+ /**
122
+ * Add an entry to the vector index (upsert - replaces if exists)
123
+ */
124
+ async addEntry(entryId: number, content: string): Promise<boolean> {
125
+ if (!this.initialized) {
126
+ await this.initialize();
127
+ }
128
+
129
+ if (!this.index) {
130
+ return false;
131
+ }
132
+
133
+ try {
134
+ // Delete existing item first to support upsert behavior
135
+ try {
136
+ await this.index.deleteItem(String(entryId));
137
+ } catch {
138
+ // Item may not exist, ignore
139
+ }
140
+
141
+ // Generate embedding
142
+ const embedding = await this.generateEmbedding(content);
143
+
144
+ // Add to vectra index with entry ID as metadata
145
+ await this.index.insertItem({
146
+ id: String(entryId),
147
+ vector: embedding,
148
+ metadata: { entryId, contentPreview: content.slice(0, 100) }
149
+ });
150
+
151
+ logger.debug('Added entry to vector index', {
152
+ module: 'VectorSearch',
153
+ entityId: entryId
154
+ });
155
+
156
+ return true;
157
+ } catch (error) {
158
+ logger.error('Failed to add entry to vector index', {
159
+ module: 'VectorSearch',
160
+ entityId: entryId,
161
+ error: error instanceof Error ? error.message : String(error)
162
+ });
163
+ return false;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Perform semantic search
169
+ */
170
+ async search(
171
+ query: string,
172
+ limit = 10,
173
+ similarityThreshold = 0.3
174
+ ): Promise<SemanticSearchResult[]> {
175
+ if (!this.initialized) {
176
+ await this.initialize();
177
+ }
178
+
179
+ if (!this.index) {
180
+ return [];
181
+ }
182
+
183
+ try {
184
+ // Generate query embedding
185
+ const queryEmbedding = await this.generateEmbedding(query);
186
+
187
+ // Search vectra index
188
+ const results = await this.index.queryItems(queryEmbedding, limit * 2);
189
+
190
+ // Filter by threshold and map to our format
191
+ const filteredResults: SemanticSearchResult[] = results
192
+ .filter(r => r.score >= similarityThreshold)
193
+ .slice(0, limit)
194
+ .map(r => ({
195
+ entryId: (r.item.metadata as { entryId: number }).entryId,
196
+ score: r.score
197
+ }));
198
+
199
+ return filteredResults;
200
+ } catch (error) {
201
+ logger.error('Semantic search failed', {
202
+ module: 'VectorSearch',
203
+ error: error instanceof Error ? error.message : String(error)
204
+ });
205
+ return [];
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Remove an entry from the vector index
211
+ */
212
+ async removeEntry(entryId: number): Promise<boolean> {
213
+ if (!this.index) return false;
214
+
215
+ try {
216
+ await this.index.deleteItem(String(entryId));
217
+ return true;
218
+ } catch {
219
+ // Item might not exist in index
220
+ return false;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Rebuild index from database entries
226
+ */
227
+ async rebuildIndex(db: SqliteAdapter): Promise<number> {
228
+ if (!this.initialized) {
229
+ await this.initialize();
230
+ }
231
+
232
+ if (!this.index) {
233
+ return 0;
234
+ }
235
+
236
+ logger.info('Rebuilding vector index from database...', { module: 'VectorSearch' });
237
+
238
+ // Get all entries
239
+ const entries = db.getRecentEntries(10000); // Get up to 10k entries
240
+
241
+ let indexed = 0;
242
+ for (const entry of entries) {
243
+ // Delete existing item first to avoid "already exists" error
244
+ try {
245
+ await this.index.deleteItem(String(entry.id));
246
+ } catch {
247
+ // Item may not exist, ignore
248
+ }
249
+
250
+ const success = await this.addEntry(entry.id, entry.content);
251
+ if (success) indexed++;
252
+ }
253
+
254
+ logger.info(`Rebuilt vector index with ${String(indexed)} entries`, { module: 'VectorSearch' });
255
+ return indexed;
256
+ }
257
+
258
+ /**
259
+ * Get index statistics
260
+ */
261
+ async getStats(): Promise<{ itemCount: number; modelName: string; dimensions: number }> {
262
+ if (!this.index) {
263
+ return { itemCount: 0, modelName: this.modelName, dimensions: EMBEDDING_DIMENSIONS };
264
+ }
265
+
266
+ try {
267
+ const items = await this.index.listItems();
268
+ return {
269
+ itemCount: items.length,
270
+ modelName: this.modelName,
271
+ dimensions: EMBEDDING_DIMENSIONS
272
+ };
273
+ } catch {
274
+ return { itemCount: 0, modelName: this.modelName, dimensions: EMBEDDING_DIMENSIONS };
275
+ }
276
+ }
277
+ }