This is page 2 of 3. Use http://codebase.md/j3k0/mcp-elastic-memory?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ └── using-git.mdc
├── .gitignore
├── BUILD-NOTES.md
├── docker-compose.yml
├── Dockerfile
├── jest.config.cjs
├── jest.config.js
├── launch.example
├── legacy
│ ├── cli.ts
│ ├── index.ts
│ ├── query-language.test.ts
│ ├── query-language.ts
│ ├── test-memory.json
│ └── types.ts
├── package.json
├── README.md
├── src
│ ├── admin-cli.ts
│ ├── ai-service.ts
│ ├── es-types.ts
│ ├── filesystem
│ │ └── index.ts
│ ├── index.ts
│ ├── json-to-es.ts
│ ├── kg-client.ts
│ ├── kg-inspection.ts
│ └── logger.ts
├── tests
│ ├── boolean-search.test.ts
│ ├── cross-zone-relations.test.ts
│ ├── empty-name-validation.test.ts
│ ├── entity-type-filtering.test.ts
│ ├── fuzzy-search.test.ts
│ ├── non-existent-entity-relationships.test.ts
│ ├── simple.test.js
│ ├── test-config.ts
│ ├── test-cross-zone.js
│ ├── test-empty-name.js
│ ├── test-non-existent-entity.js
│ ├── test-relationship-cleanup.js
│ ├── test-relevance-score.js
│ └── test-zone-management.js
├── tsconfig.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/src/ai-service.ts:
--------------------------------------------------------------------------------
```typescript
1 | 'use strict';
2 |
3 | import logger from './logger.js';
4 |
5 | // Add Node.js process type definition
6 | declare const process: {
7 | env: {
8 | [key: string]: string | undefined;
9 | GROQ_API_KEY?: string;
10 | GROQ_MODELS?: string;
11 | DEBUG_AI?: string;
12 | };
13 | };
14 |
15 | /**
16 | * Configuration for Groq API
17 | * @constant {Object}
18 | */
19 | const GROQ_CONFIG = {
20 | baseUrl: 'https://api.groq.com/openai/v1',
21 | models: getModels(),
22 | apiKey: process.env.GROQ_API_KEY
23 | };
24 |
25 | function getModels() {
26 | if (process.env.GROQ_MODELS) {
27 | return process.env.GROQ_MODELS.split(',').map(x => x.trim()).filter(x => x);
28 | }
29 | return [
30 | 'deepseek-r1-distill-llama-70b',
31 | 'llama-3.3-70b-versatile',
32 | 'llama-3.3-70b-specdec'
33 | ]
34 | }
35 |
36 | /**
37 | * Rate limiting configuration
38 | * @constant {Object}
39 | */
40 | const RATE_LIMIT_CONFIG = {
41 | disableDuration: 5 * 60 * 1000, // 5 minutes in milliseconds
42 | };
43 |
44 | /**
45 | * Implementation of the AI filter service using Groq
46 | */
47 | export const GroqAI = {
48 | name: 'Groq',
49 | isEnabled: !!GROQ_CONFIG.apiKey,
50 |
51 | /**
52 | * Tracks if the AI service is temporarily disabled due to rate limiting
53 | * @private
54 | */
55 | isDisabled: false,
56 |
57 | /**
58 | * Timestamp when the service can be re-enabled
59 | * @private
60 | */
61 | disabledUntil: null,
62 |
63 | /**
64 | * Current index in the models array being used
65 | * @private
66 | */
67 | currentModelIndex: 0,
68 |
69 | /**
70 | * Timestamp when to attempt using a higher priority model
71 | * @private
72 | */
73 | upgradeAttemptTime: null,
74 |
75 | /**
76 | * Moves to the next fallback model in the priority list
77 | * @private
78 | * @returns {boolean} False if we've reached the end of the model list
79 | */
80 | _moveToNextModel() {
81 | if (this.currentModelIndex < GROQ_CONFIG.models.length - 1) {
82 | this.currentModelIndex++;
83 | this.upgradeAttemptTime = Date.now() + RATE_LIMIT_CONFIG.disableDuration;
84 | logger.warn(`Switching to model ${GROQ_CONFIG.models[this.currentModelIndex]} until ${new Date(this.upgradeAttemptTime).toISOString()}`);
85 | return true;
86 | }
87 |
88 | // No more models available, disable the service
89 | this.isDisabled = true;
90 | this.disabledUntil = Date.now() + RATE_LIMIT_CONFIG.disableDuration;
91 | logger.warn(`All models exhausted. Service disabled until ${new Date(this.disabledUntil).toISOString()}`);
92 | return false;
93 | },
94 |
95 | /**
96 | * Checks if we should attempt to upgrade to a higher priority model
97 | * @private
98 | */
99 | _checkUpgrade() {
100 | const now = Date.now();
101 | if (this.currentModelIndex > 0 && this.upgradeAttemptTime && now >= this.upgradeAttemptTime) {
102 | this.currentModelIndex--;
103 | this.upgradeAttemptTime = null;
104 | logger.info(`Attempting to upgrade to model ${GROQ_CONFIG.models[this.currentModelIndex]}`);
105 | }
106 | },
107 |
108 | /**
109 | * Checks if the service is currently disabled and can be re-enabled
110 | * @private
111 | */
112 | _checkStatus() {
113 | const now = Date.now();
114 |
115 | if (this.isDisabled && now >= this.disabledUntil) {
116 | this.isDisabled = false;
117 | this.disabledUntil = null;
118 | this.currentModelIndex = 0; // Reset to highest priority model
119 | this.upgradeAttemptTime = null;
120 | logger.info('AI service re-enabled with primary model');
121 | }
122 |
123 | this._checkUpgrade();
124 |
125 | return {
126 | isDisabled: this.isDisabled,
127 | currentModel: GROQ_CONFIG.models[this.currentModelIndex]
128 | };
129 | },
130 |
131 | /**
132 | * Filters search results using AI to determine which entities are relevant to the user's information needs
133 | * @param {Object[]} searchResults - Array of entity objects from search
134 | * @param {string} userinformationNeeded - Description of what the user is looking for
135 | * @param {string} [reason] - Reason for the search, providing additional context
136 | * @returns {Promise<Record<string, number>>} Object mapping entity names to usefulness scores (0-100)
137 | * @throws {Error} If the API request fails
138 | */
139 | async filterSearchResults(searchResults, userinformationNeeded, reason) {
140 |
141 | const ret = searchResults.reduce((acc, result) => {
142 | acc[result.name] = 40;
143 | return acc;
144 | }, {});
145 |
146 | if (!userinformationNeeded || !searchResults || searchResults.length === 0) {
147 | return null; // Return null to tell the caller to use the original results
148 | }
149 |
150 | const status = this._checkStatus();
151 |
152 | if (status.isDisabled) {
153 | // If AI service is disabled, return null
154 | logger.warn('AI service temporarily disabled, returning null to use original results');
155 | return null;
156 | }
157 |
158 | const systemPrompt = `You are an intelligent filter for a knowledge graph search.
159 | Your task is to analyze search results and determine which entities are useful to the user's information needs.
160 | Usefulness will be a score between 0 and 100:
161 | - < 10: definitely not useful
162 | - < 50: quite not useful
163 | - >= 50: useful
164 | - >= 90: extremely useful
165 | Do not include entities with a score between 10 and 50 in your response.
166 | Return a JSON object with the entity names as keys and their usefulness scores as values. Nothing else.`;
167 |
168 | let userPrompt = `Why am I searching: ${userinformationNeeded}`;
169 |
170 | if (reason) {
171 | userPrompt += `\nReason for search: ${reason}`;
172 | }
173 |
174 | userPrompt += `\n\nHere are the search results to filter:
175 | ${JSON.stringify(searchResults, null, 2)}
176 |
177 | Return a JSON object mapping entity names to their usefulness scores (0-100). Do not omit any entities.
178 | IMPORTANT: Your response will be directly passed to JSON.parse(). Do NOT use markdown formatting, code blocks, or any other formatting. Return ONLY a raw, valid JSON object.`;
179 |
180 | try {
181 | const response = await this.chatCompletion({
182 | system: systemPrompt,
183 | user: userPrompt
184 | });
185 |
186 | // Handle the response based on its type
187 | if (typeof response === 'object' && !Array.isArray(response)) {
188 | // If response is already an object, add entities with scores between 10 and 50,
189 | // and include entities with scores >= 50
190 | Object.entries(response).forEach(([name, score]) => {
191 | if (typeof score === 'number') {
192 | ret[name] = score;
193 | }
194 | });
195 |
196 | return ret;
197 | } else if (typeof response === 'string') {
198 | // If response is a string, try to parse it as JSON
199 | try {
200 | const parsedResponse = JSON.parse(response);
201 |
202 | if (typeof parsedResponse === 'object' && !Array.isArray(parsedResponse)) {
203 | // If parsed response is an object, add entities with scores between 10 and 50,
204 | // and include entities with scores >= 50
205 | Object.entries(parsedResponse).forEach(([name, score]) => {
206 | if (typeof score === 'number') {
207 | ret[name] = score;
208 | }
209 | });
210 |
211 | return ret;
212 | } else if (Array.isArray(parsedResponse)) {
213 | // For backward compatibility: if response is an array of entity names,
214 | // convert to object with maximum usefulness for each entity
215 | logger.warn('Received array format instead of object with scores, returning null to use original results');
216 | return null;
217 | } else {
218 | logger.warn('Unexpected response format from AI, returning null to use original results', { response });
219 | return null;
220 | }
221 | } catch (error) {
222 | logger.error('Error parsing AI response, returning null to use original results', { error, response });
223 | return null;
224 | }
225 | } else if (Array.isArray(response)) {
226 | // For backward compatibility: if response is an array of entity names,
227 | // convert to object with maximum usefulness for each entity
228 | logger.warn('Received array format instead of object with scores, returning null to use original results');
229 | return null;
230 | } else {
231 | // For any other type of response, return null
232 | logger.warn('Unhandled response type from AI, returning null to use original results', { responseType: typeof response });
233 | return null;
234 | }
235 | } catch (error) {
236 | logger.error('Error calling AI service, returning null to use original results', { error });
237 | return null;
238 | }
239 | },
240 |
241 | /**
242 | * Helper function to safely parse JSON with multiple attempts
243 | * @private
244 | * @param {string} jsonString - The JSON string to parse
245 | * @returns {Object|null} Parsed object or null if parsing fails
246 | */
247 | _safeJsonParse(jsonString) {
248 | // First attempt: direct parsing
249 | try {
250 | return JSON.parse(jsonString);
251 | } catch (error) {
252 | if (process.env.DEBUG_AI === 'true') {
253 | logger.debug('First JSON parse attempt failed, trying to clean the string', { error: error.message });
254 | }
255 |
256 | // Second attempt: try to extract JSON from markdown code blocks
257 | try {
258 | const matches = jsonString.match(/```(?:json)?\s*([\s\S]*?)```/);
259 | if (matches && matches[1]) {
260 | const extracted = matches[1].trim();
261 | return JSON.parse(extracted);
262 | }
263 | } catch (error) {
264 | if (process.env.DEBUG_AI === 'true') {
265 | logger.debug('Second JSON parse attempt failed', { error: error.message });
266 | }
267 | }
268 |
269 | // Third attempt: try to find anything that looks like a JSON object
270 | try {
271 | const jsonRegex = /{[^]*}/;
272 | const matches = jsonString.match(jsonRegex);
273 | if (matches && matches[0]) {
274 | return JSON.parse(matches[0]);
275 | }
276 | } catch (error) {
277 | if (process.env.DEBUG_AI === 'true') {
278 | logger.debug('Third JSON parse attempt failed', { error: error.message });
279 | }
280 | }
281 |
282 | // All attempts failed
283 | return null;
284 | }
285 | },
286 |
287 | /**
288 | * Sends a prompt to the Groq AI and returns the response
289 | * @param {Object} data - The chat completion request data
290 | * @param {string} data.system - The system message
291 | * @param {string} data.user - The user message
292 | * @param {Object} [data.model] - Optional model override
293 | * @returns {Promise<string>} The response from the AI
294 | * @throws {Error} If the API request fails
295 | */
296 | async chatCompletion(data) {
297 | const status = this._checkStatus();
298 |
299 | if (status.isDisabled) {
300 | throw new Error('AI service temporarily disabled due to rate limiting');
301 | }
302 |
303 | const messages = [
304 | { role: 'system', content: data.system },
305 | { role: 'user', content: data.user }
306 | ];
307 |
308 | const modelToUse = data.model?.model || status.currentModel;
309 |
310 | try {
311 | const response = await fetch(`${GROQ_CONFIG.baseUrl}/chat/completions`, {
312 | method: 'POST',
313 | headers: {
314 | 'Content-Type': 'application/json',
315 | 'Accept': 'application/json',
316 | 'Authorization': `Bearer ${GROQ_CONFIG.apiKey}`
317 | },
318 | body: JSON.stringify({
319 | model: modelToUse,
320 | messages,
321 | max_tokens: 1000,
322 | temperature: 0.25,
323 | }),
324 | });
325 |
326 | logger.info('Groq API response:', { status: response.status, statusText: response.statusText });
327 |
328 | if (!response.ok) {
329 | if (response.status === 429) { // Too Many Requests
330 | if (this._moveToNextModel()) {
331 | return this.chatCompletion(data);
332 | }
333 | }
334 | throw new Error(`Groq API error: ${response.statusText}`);
335 | }
336 |
337 | const result = await response.json();
338 | const content = result.choices[0].message.content;
339 |
340 | // Clean up the response by removing <think>...</think> tags if present
341 | let cleanedContent = content;
342 |
343 | // Only process if content is a string
344 | if (typeof content === 'string') {
345 | logger.info('Groq API response content:', { content });
346 | if (content.includes('<think>')) {
347 | const thinkingTagRegex = /<think>[\s\S]*?<\/think>/g;
348 | cleanedContent = content.replace(thinkingTagRegex, '').trim();
349 |
350 | // Log the cleaning if in debug mode
351 | if (process.env.DEBUG_AI === 'true') {
352 | logger.debug('Cleaned AI response by removing thinking tags');
353 | }
354 | }
355 |
356 | try {
357 | // Try to parse as JSON if it looks like JSON
358 | if ((cleanedContent.startsWith('{') && cleanedContent.endsWith('}')) ||
359 | (cleanedContent.startsWith('[') && cleanedContent.endsWith(']'))) {
360 | const parsed = JSON.parse(cleanedContent);
361 | return parsed;
362 | }
363 | } catch (error) {
364 | // Try additional parsing strategies
365 | const parsed = this._safeJsonParse(cleanedContent);
366 | if (parsed) {
367 | if (process.env.DEBUG_AI === 'true') {
368 | logger.debug('Recovered JSON using safe parsing method');
369 | }
370 | return parsed;
371 | }
372 |
373 | // If parsing fails, return cleaned string content
374 | if (process.env.DEBUG_AI === 'true') {
375 | logger.debug('Failed to parse response as JSON:', error.message);
376 | }
377 | }
378 | } else if (typeof content === 'object') {
379 | // If the content is already an object, return it directly
380 | return content;
381 | }
382 |
383 | return cleanedContent;
384 | } catch (error) {
385 | if (error.message.includes('Too Many Requests')) {
386 | if (this._moveToNextModel()) {
387 | return this.chatCompletion(data);
388 | }
389 | }
390 | throw error;
391 | }
392 | },
393 |
394 | /**
395 | * Classifies zones by usefulness based on the reason for listing
396 | * @param {ZoneMetadata[]} zones - Array of zone metadata objects
397 | * @param {string} reason - The reason for listing zones
398 | * @returns {Promise<Record<string, number>>} Object mapping zone names to usefulness scores (0-2)
399 | */
400 | async classifyZoneUsefulness(zones, reason) {
401 | if (!reason || !zones || zones.length === 0) {
402 | return {};
403 | }
404 |
405 | const status = this._checkStatus();
406 |
407 | if (status.isDisabled) {
408 | // If AI service is disabled, return all zones as very useful
409 | logger.warn('AI service temporarily disabled, returning all zones as very useful');
410 | return zones.reduce((acc, zone) => {
411 | acc[zone.name] = 2; // all zones marked as very useful
412 | return acc;
413 | }, {});
414 | }
415 |
416 | const systemPrompt = `You are an intelligent zone classifier for a knowledge graph system.
417 | Your task is to analyze memory zones and determine how useful each zone is to the user's current needs.
418 | Rate each zone on a scale from 0-2:
419 | 0: not useful
420 | 1: a little useful
421 | 2: very useful
422 |
423 | Return ONLY a JSON object mapping zone names to usefulness scores. Format: {"zoneName": usefulness}`;
424 |
425 | const zoneData = zones.map(zone => ({
426 | name: zone.name,
427 | description: zone.description || ''
428 | }));
429 |
430 | const userPrompt = `Reason for listing zones: ${reason}
431 |
432 | Here are the zones to classify:
433 | ${JSON.stringify(zoneData, null, 2)}
434 |
435 | Return a JSON object mapping each zone name to its usefulness score (0-2):
436 | 0: not useful for my reason
437 | 1: a little useful for my reason
438 | 2: very useful for my reason`;
439 |
440 | try {
441 | const response = await this.chatCompletion({
442 | system: systemPrompt,
443 | user: userPrompt
444 | });
445 |
446 | // Parse the response - expecting a JSON object mapping zone names to scores
447 | try {
448 | const parsedResponse = JSON.parse(response);
449 | if (typeof parsedResponse === 'object' && !Array.isArray(parsedResponse)) {
450 | // Validate scores are in range 0-2
451 | Object.keys(parsedResponse).forEach(zoneName => {
452 | const score = parsedResponse[zoneName];
453 | if (typeof score !== 'number' || score < 0 || score > 2) {
454 | parsedResponse[zoneName] = 2; // Default to very useful for invalid scores
455 | }
456 | });
457 | return parsedResponse;
458 | } else {
459 | logger.warn('Unexpected response format from AI, returning all zones as very useful', { response });
460 | return zones.reduce((acc, zone) => {
461 | acc[zone.name] = 2; // all zones marked as very useful
462 | return acc;
463 | }, {});
464 | }
465 | } catch (error) {
466 | logger.error('Error parsing AI response, returning all zones as very useful', { error, response });
467 | return zones.reduce((acc, zone) => {
468 | acc[zone.name] = 2; // all zones marked as very useful
469 | return acc;
470 | }, {});
471 | }
472 | } catch (error) {
473 | logger.error('Error calling AI service, returning all zones as very useful', { error });
474 | return zones.reduce((acc, zone) => {
475 | acc[zone.name] = 2; // all zones marked as very useful
476 | return acc;
477 | }, {});
478 | }
479 | },
480 |
481 | /**
482 | * Generates descriptions for a zone based on its content
483 | * @param {string} zoneName - The name of the zone
484 | * @param {string} currentDescription - The current description of the zone (if any)
485 | * @param {Array} relevantEntities - Array of the most relevant entities in the zone
486 | * @param {string} [userPrompt] - Optional user-provided description of the zone's purpose
487 | * @returns {Promise<{description: string, shortDescription: string}>} Generated descriptions
488 | */
489 | async generateZoneDescriptions(zoneName, currentDescription, relevantEntities, userPrompt): Promise<{description: string, shortDescription: string}> {
490 | if (!relevantEntities || relevantEntities.length === 0) {
491 | return {
492 | description: currentDescription || `Zone: ${zoneName}`,
493 | shortDescription: currentDescription || zoneName
494 | };
495 | }
496 |
497 | const status = this._checkStatus();
498 |
499 | if (status.isDisabled) {
500 | // If AI service is disabled, return current description
501 | logger.warn('AI service temporarily disabled, returning existing description');
502 | return {
503 | description: currentDescription || `Zone: ${zoneName}`,
504 | shortDescription: currentDescription || zoneName
505 | };
506 | }
507 |
508 | const systemPrompt = `You are an AI assistant that generates concise and informative descriptions for memory zones in a knowledge graph system.
509 | Your primary task is to answer the question: "What is ${zoneName}?" based on the content within this zone.
510 |
511 | Based on the zone name and the entities it contains, create two descriptions:
512 | 1. A full description (up to 200 words) that explains what this zone is, its purpose, and content in detail. This should clearly answer "What is ${zoneName}? No general bulshit, focus on the specifics, what makes unique."
513 | 2. A short description (15-25 words) that succinctly explains what ${zoneName} is.
514 |
515 | Your descriptions should be clear, informative, and accurately reflect the zone's content.
516 | Avoid using generic phrases like "This zone contains..." or "A collection of...".
517 | Instead, focus on the specific subject matter and purpose of the zone.
518 |
519 | IMPORTANT: Your response will be directly passed to JSON.parse(). Do NOT use markdown formatting, code blocks, or any other formatting. Return ONLY a raw, valid JSON object with "description" and "shortDescription" fields. For example:
520 | {"description": "This is a description", "shortDescription": "Short description"}`;
521 |
522 | let userPromptText = `Zone name: ${zoneName}
523 | Current description: ${currentDescription || 'None'}
524 |
525 | Here are the most relevant entities in this zone:
526 | ${JSON.stringify(relevantEntities, null, 2)}`;
527 |
528 | // If user provided additional context, include it
529 | if (userPrompt) {
530 | userPromptText += `\n\nUser-provided zone purpose: ${userPrompt}`;
531 | }
532 |
533 | userPromptText += `\n\nGenerate two descriptions that answer "What is ${zoneName}?":
534 | 1. A full description (up to 200 words)
535 | 2. A short description (15-25 words)
536 |
537 | Return your response as a raw, valid JSON object with "description" and "shortDescription" fields. Do NOT use markdown formatting, code blocks or any other formatting. Just the raw JSON object.`;
538 |
539 | try {
540 | const response = await this.chatCompletion({
541 | system: systemPrompt,
542 | user: userPromptText
543 | });
544 |
545 | // Log the raw response for debugging purposes
546 | if (process.env.DEBUG_AI === 'true') {
547 | logger.debug('Raw AI response:', response);
548 | }
549 |
550 | // Handle the response
551 | try {
552 | // If the response is already an object with the expected format, use it directly
553 | if (typeof response === 'object' &&
554 | typeof response.description === 'string' &&
555 | typeof response.shortDescription === 'string') {
556 | return {
557 | description: response.description,
558 | shortDescription: response.shortDescription
559 | };
560 | }
561 |
562 | // If the response is a string, try to parse it
563 | if (typeof response === 'string') {
564 | // Try to parse with enhanced parsing function
565 | const parsedResponse = this._safeJsonParse(response);
566 |
567 | if (parsedResponse &&
568 | typeof parsedResponse.description === 'string' &&
569 | typeof parsedResponse.shortDescription === 'string') {
570 | return {
571 | description: parsedResponse.description,
572 | shortDescription: parsedResponse.shortDescription
573 | };
574 | }
575 | }
576 |
577 | // If we get here, the response format is unexpected
578 | logger.warn('Unexpected response format from AI, returning existing description', { response });
579 | return {
580 | description: currentDescription || `Zone: ${zoneName}`,
581 | shortDescription: currentDescription || zoneName
582 | };
583 | } catch (error) {
584 | logger.error('Error parsing AI response, returning existing description', { error, response });
585 | return {
586 | description: currentDescription || `Zone: ${zoneName}`,
587 | shortDescription: currentDescription || zoneName
588 | };
589 | }
590 | } catch (error) {
591 | logger.error('Error calling AI service, returning existing description', { error });
592 | return {
593 | description: currentDescription || `Zone: ${zoneName}`,
594 | shortDescription: currentDescription || zoneName
595 | };
596 | }
597 | },
598 |
599 | /**
600 | * Analyzes file content and returns lines relevant to the user's information needs
601 | * @param {Array<{lineNumber: number, content: string}>} fileLines - Array of line objects with line numbers and content
602 | * @param {string} informationNeeded - Description of what information is needed from the file
603 | * @param {string} [reason] - Additional context about why this information is needed
604 | * @returns {Promise<Array<{lineNumber: number, content: string, relevance: number}>>} Array of relevant lines with their relevance scores
605 | * @throws {Error} If the API request fails
606 | */
607 | async filterFileContent(fileLines, informationNeeded, reason): Promise<{lineRanges: string[], tentativeAnswer?: string}> {
608 | if (!informationNeeded || !fileLines || fileLines.length === 0) {
609 | return {
610 | lineRanges: [`1-${fileLines.length}`],
611 | tentativeAnswer: "No information needed, returning all lines"
612 | };
613 | }
614 |
615 | const status = this._checkStatus();
616 |
617 | if (status.isDisabled) {
618 | logger.warn('AI service temporarily disabled, returning all lines');
619 | return {
620 | lineRanges: [`1-${fileLines.length}`],
621 | tentativeAnswer: "Groq AI service is temporarily disabled. Please try again later."
622 | };
623 | }
624 |
625 | const systemPrompt = `You are an intelligent file content analyzer.
626 | Your task is to analyze file contents and determine which lines are relevant to the user's information needs.
627 | The response should be a raw JSON object like: {"lineRanges": ["1-10", "20-40", ...], "tentativeAnswer": "Answer to the information needed, if possible."}
628 | Be selective, the goal is to find the most relevant lines, not to include all of them. If some lines might be relevant but not worth returning completely, the tentative answer can mention additional line range with a short description.
629 | `;
630 |
631 | let userPrompt = `Information needed: ${informationNeeded}`;
632 |
633 | if (reason) {
634 | userPrompt += `\nContext/Reason: ${reason}`;
635 | }
636 |
637 | userPrompt += `\n\nHere are the file contents to analyze (<line number>:<content>):
638 | ${fileLines.map(line => `${line.lineNumber}:${line.content}`).slice(0, 2000).join('\n')}
639 |
640 | Return a JSON object with: {
641 | "temptativeAnswer": "Answer to the information needed, if possible. Do not be too general, be specific. Make it detailed, but without useless details. It must be straight to the point, using as little words as posssible without losing information. The text can be long (even 100 words or more if necessary), it's a good thing as long as it's relevant and based on facts based on the file content. But information must be condensed, don't be too verbose.",
642 | "lineRanges": ["1-10", "20-40", ...]
643 | }
644 | IMPORTANT: Your response must be a raw JSON object that can be parsed with JSON.parse().`;
645 |
646 | try {
647 | const response = await this.chatCompletion({
648 | system: systemPrompt,
649 | user: userPrompt
650 | });
651 |
652 | let result: {lineRanges: string[], tentativeAnswer?: string};
653 | if (typeof response === 'object' && !Array.isArray(response)) {
654 | result = response;
655 | if (!result.lineRanges || !Array.isArray(result.lineRanges)) {
656 | result.lineRanges = [];
657 | }
658 | } else if (typeof response === 'string') {
659 | // Try to parse with enhanced parsing function
660 | const parsedResponse = this._safeJsonParse(response);
661 |
662 | if (parsedResponse) {
663 | result = parsedResponse;
664 | if (!result.lineRanges || !Array.isArray(result.lineRanges)) {
665 | result.lineRanges = [];
666 | }
667 | } else {
668 | logger.error('Error parsing AI response, returning all lines', { response });
669 | return {
670 | lineRanges: [`1-${fileLines.length}`],
671 | tentativeAnswer: "Error parsing AI response, returning all lines"
672 | };
673 | }
674 | }
675 |
676 | // Filter and format the results
677 | return {
678 | lineRanges: result.lineRanges,
679 | tentativeAnswer: result.tentativeAnswer || "No answers given by AI"
680 | }
681 | } catch (error) {
682 | logger.error('Error calling AI service, returning all lines', { error });
683 | return {
684 | lineRanges: [`1-${fileLines.length}`],
685 | tentativeAnswer: "Error calling AI service, returning all lines"
686 | };
687 | }
688 | },
689 | };
690 |
691 | export default GroqAI;
```
--------------------------------------------------------------------------------
/src/admin-cli.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { promises as fs } from 'fs';
4 | import path from 'path';
5 | import { KnowledgeGraphClient } from './kg-client.js';
6 | import { ESEntity, ESRelation, KG_RELATIONS_INDEX } from './es-types.js';
7 | import { importFromJsonFile, exportToJsonFile } from './json-to-es.js';
8 | import readline from 'readline';
9 | import GroqAI from './ai-service.js';
10 |
11 | // Environment configuration for Elasticsearch
12 | const ES_NODE = process.env.ES_NODE || 'http://localhost:9200';
13 | const ES_USERNAME = process.env.ES_USERNAME;
14 | const ES_PASSWORD = process.env.ES_PASSWORD;
15 | const DEFAULT_ZONE = process.env.KG_DEFAULT_ZONE || 'default';
16 |
17 | // Configure ES client with authentication if provided
18 | const esOptions: {
19 | node: string;
20 | auth?: { username: string; password: string };
21 | defaultZone?: string;
22 | } = {
23 | node: ES_NODE,
24 | defaultZone: DEFAULT_ZONE
25 | };
26 |
27 | if (ES_USERNAME && ES_PASSWORD) {
28 | esOptions.auth = { username: ES_USERNAME, password: ES_PASSWORD };
29 | }
30 |
31 | // Create KG client
32 | const kgClient = new KnowledgeGraphClient(esOptions);
33 |
34 | /**
35 | * Display help information
36 | */
37 | function showHelp() {
38 | console.log('Knowledge Graph Admin CLI');
39 | console.log('========================');
40 | console.log('');
41 | console.log('Commands:');
42 | console.log(' init Initialize the Elasticsearch index');
43 | console.log(' import <file> [zone] Import data from a JSON file (optionally to a specific zone)');
44 | console.log(' export <file> [zone] Export data to a JSON file (optionally from a specific zone)');
45 | console.log(' backup <file> Backup all zones and relations to a file');
46 | console.log(' restore <file> [--yes] Restore all zones and relations from a backup file');
47 | console.log(' stats [zone] Display statistics about the knowledge graph');
48 | console.log(' search <query> [zone] Search the knowledge graph');
49 | console.log(' reset [zone] [--yes] Reset the knowledge graph (delete all data)');
50 | console.log(' entity <n> [zone] Display information about a specific entity');
51 | console.log(' zones list List all memory zones');
52 | console.log(' zones add <name> [desc] Add a new memory zone');
53 | console.log(' zones delete <name> [--yes] Delete a memory zone and all its data');
54 | console.log(' zones stats <name> Show statistics for a specific zone');
55 | console.log(' zones update_descriptions <name> [limit] [prompt]');
56 | console.log(' Generate AI descriptions based on zone content');
57 | console.log(' (limit: optional entity limit, prompt: optional description of zone purpose)');
58 | console.log(' relations <entity> [zone] Show relations for a specific entity');
59 | console.log(' help Show this help information');
60 | console.log('');
61 | console.log('Options:');
62 | console.log(' --yes, -y Automatically confirm all prompts (for scripts)');
63 | console.log('');
64 | console.log('Environment variables:');
65 | console.log(' ES_NODE Elasticsearch node URL (default: http://localhost:9200)');
66 | console.log(' ES_USERNAME Elasticsearch username (if authentication is required)');
67 | console.log(' ES_PASSWORD Elasticsearch password (if authentication is required)');
68 | console.log(' KG_DEFAULT_ZONE Default zone to use (default: "default")');
69 | }
70 |
71 | /**
72 | * Initialize the Elasticsearch index
73 | */
74 | async function initializeIndex() {
75 | try {
76 | await kgClient.initialize();
77 | console.log('Elasticsearch indices initialized successfully');
78 | } catch (error) {
79 | console.error('Error initializing index:', (error as Error).message);
80 | process.exit(1);
81 | }
82 | }
83 |
84 | /**
85 | * Display statistics about the knowledge graph or a specific zone
86 | */
87 | async function showStats(zone?: string) {
88 | try {
89 | // Initialize client
90 | await kgClient.initialize(zone);
91 |
92 | if (zone) {
93 | // Get zone-specific stats
94 | const stats = await kgClient.getMemoryZoneStats(zone);
95 |
96 | console.log(`Knowledge Graph Statistics for Zone: ${zone}`);
97 | console.log('=============================================');
98 | console.log(`Total entities: ${stats.entityCount}`);
99 | console.log(`Total relations: ${stats.relationCount}`);
100 | console.log('');
101 |
102 | console.log('Entity types:');
103 | Object.entries(stats.entityTypes).forEach(([type, count]) => {
104 | console.log(` ${type}: ${count}`);
105 | });
106 | console.log('');
107 |
108 | console.log('Relation types:');
109 | Object.entries(stats.relationTypes).forEach(([type, count]) => {
110 | console.log(` ${type}: ${count}`);
111 | });
112 | } else {
113 | // Get all zone metadata
114 | const zones = await kgClient.listMemoryZones();
115 | console.log('Knowledge Graph Multi-zone Statistics');
116 | console.log('====================================');
117 | console.log(`Total zones: ${zones.length}`);
118 | console.log('');
119 |
120 | console.log('Zones:');
121 | for (const zone of zones) {
122 | console.log(`Zone: ${zone.name}`);
123 | console.log(` Description: ${zone.description || 'N/A'}`);
124 | console.log(` Created: ${zone.createdAt}`);
125 | console.log(` Last modified: ${zone.lastModified}`);
126 |
127 | // Get zone stats
128 | const stats = await kgClient.getMemoryZoneStats(zone.name);
129 | console.log(` Entities: ${stats.entityCount}`);
130 | console.log(` Relations: ${stats.relationCount}`);
131 | console.log('');
132 | }
133 |
134 | // Get relation stats
135 | const data = await kgClient.exportData();
136 | const relations = data.filter(item => item.type === 'relation');
137 |
138 | console.log(`Total relations in all zones: ${relations.length}`);
139 |
140 | // Count relation types
141 | const relationTypes = new Map<string, number>();
142 | relations.forEach(relation => {
143 | const type = (relation as any).relationType;
144 | relationTypes.set(type, (relationTypes.get(type) || 0) + 1);
145 | });
146 |
147 | console.log('Relation types:');
148 | relationTypes.forEach((count, type) => {
149 | console.log(` ${type}: ${count}`);
150 | });
151 | }
152 | } catch (error) {
153 | console.error('Error getting statistics:', (error as Error).message);
154 | process.exit(1);
155 | }
156 | }
157 |
158 | /**
159 | * Search the knowledge graph
160 | * @param query The search query
161 | * @param zone Optional zone to search in
162 | */
163 | async function searchGraph(query: string, zone?: string) {
164 | try {
165 | // Initialize client
166 | await kgClient.initialize(zone);
167 |
168 | // Search for entities
169 | const results = await kgClient.search({
170 | query,
171 | limit: 10,
172 | sortBy: 'relevance',
173 | zone
174 | });
175 |
176 | // Display results
177 | console.log(`Search Results for "${query}"${zone ? ` in zone "${zone}"` : ''}`);
178 | console.log('====================================');
179 | console.log(`Found ${results.hits.total.value} matches`);
180 | console.log('');
181 |
182 | // Extract all entities from search results
183 | const entities = results.hits.hits
184 | .filter(hit => hit._source.type === 'entity')
185 | .map(hit => hit._source as ESEntity);
186 |
187 | // Get entity names for relation lookup
188 | const entityNames = entities.map(entity => entity.name);
189 |
190 | // Create a set of entity names for faster lookup
191 | const entityNameSet = new Set(entityNames);
192 |
193 | // Collect all relations
194 | const allRelations: ESRelation[] = [];
195 | const relatedEntities = new Map<string, ESEntity>();
196 |
197 | // For each found entity, get all its relations
198 | for (const entityName of entityNames) {
199 | const { relations } = await kgClient.getRelatedEntities(entityName, 1, zone);
200 |
201 | for (const relation of relations) {
202 | // Add relation if not already added
203 | if (!allRelations.some(r =>
204 | r.from === relation.from &&
205 | r.fromZone === relation.fromZone &&
206 | r.to === relation.to &&
207 | r.toZone === relation.toZone &&
208 | r.relationType === relation.relationType
209 | )) {
210 | allRelations.push(relation);
211 |
212 | // Track related entities that weren't in the search results
213 | // If 'from' entity is not in our set and not already tracked
214 | if (!entityNameSet.has(relation.from) && !relatedEntities.has(relation.from)) {
215 | const entity = await kgClient.getEntityWithoutUpdatingLastRead(relation.from, relation.fromZone);
216 | if (entity) relatedEntities.set(relation.from, entity);
217 | }
218 |
219 | // If 'to' entity is not in our set and not already tracked
220 | if (!entityNameSet.has(relation.to) && !relatedEntities.has(relation.to)) {
221 | const entity = await kgClient.getEntityWithoutUpdatingLastRead(relation.to, relation.toZone);
222 | if (entity) relatedEntities.set(relation.to, entity);
223 | }
224 | }
225 | }
226 | }
227 |
228 | // Display each entity from search results
229 | entities.forEach((entity, index) => {
230 | const hit = results.hits.hits.find(h =>
231 | h._source.type === 'entity' && (h._source as ESEntity).name === entity.name
232 | );
233 | const score = hit && hit._score !== null && hit._score !== undefined ? hit._score.toFixed(2) : 'N/A';
234 |
235 | console.log(`${index + 1}. ${entity.name} (${entity.entityType}) [Score: ${score}]`);
236 | console.log(` Zone: ${entity.zone || 'default'}`);
237 | console.log(` Observations: ${entity.observations.length}`);
238 |
239 | // Show highlights if available
240 | if (hit && hit.highlight) {
241 | console.log(' Matches:');
242 | Object.entries(hit.highlight).forEach(([field, highlights]) => {
243 | highlights.forEach(highlight => {
244 | console.log(` - ${field}: ${highlight}`);
245 | });
246 | });
247 | }
248 | console.log('');
249 | });
250 |
251 | // Display relations if any
252 | if (allRelations.length > 0) {
253 | console.log('Relations for these entities:');
254 | console.log('====================================');
255 |
256 | allRelations.forEach(relation => {
257 | // Lookup entity types for more context
258 | const fromType = entityNameSet.has(relation.from)
259 | ? entities.find(e => e.name === relation.from)?.entityType
260 | : relatedEntities.get(relation.from)?.entityType || '?';
261 |
262 | const toType = entityNameSet.has(relation.to)
263 | ? entities.find(e => e.name === relation.to)?.entityType
264 | : relatedEntities.get(relation.to)?.entityType || '?';
265 |
266 | console.log(`${relation.from} [${relation.fromZone}] (${fromType}) → ${relation.relationType} → ${relation.to} [${relation.toZone}] (${toType})`);
267 | });
268 | console.log('');
269 | }
270 |
271 | } catch (error) {
272 | console.error('Error searching knowledge graph:', (error as Error).message);
273 | process.exit(1);
274 | }
275 | }
276 |
277 | /**
278 | * Reset the knowledge graph (delete all data)
279 | * @param zone Optional zone to reset, if not provided resets all zones
280 | */
281 | async function resetIndex(zone?: string, args: string[] = []) {
282 | try {
283 | const confirmMessage = zone
284 | ? `Are you sure you want to delete all data in zone "${zone}"? This cannot be undone. (y/N) `
285 | : 'Are you sure you want to delete ALL DATA IN ALL ZONES? This cannot be undone. (y/N) ';
286 |
287 | const confirmed = await confirmAction(confirmMessage, args);
288 |
289 | if (confirmed) {
290 | if (zone) {
291 | // Delete specific zone
292 | if (zone === 'default') {
293 | // For default zone, just delete all entities but keep the index
294 | await kgClient.initialize(zone);
295 | const allEntities = await kgClient.exportData(zone);
296 | for (const item of allEntities) {
297 | if (item.type === 'entity') {
298 | await kgClient.deleteEntity(item.name, zone);
299 | }
300 | }
301 |
302 | // Delete relations involving this zone
303 | const client = kgClient['client']; // Access the private client property
304 | await client.deleteByQuery({
305 | index: KG_RELATIONS_INDEX,
306 | body: {
307 | query: {
308 | bool: {
309 | should: [
310 | { term: { fromZone: zone } },
311 | { term: { toZone: zone } }
312 | ],
313 | minimum_should_match: 1
314 | }
315 | }
316 | },
317 | refresh: true
318 | });
319 |
320 | console.log(`Zone "${zone}" has been reset (entities and relations deleted)`);
321 | } else {
322 | // For non-default zones, delete the zone completely
323 | const success = await kgClient.deleteMemoryZone(zone);
324 | if (success) {
325 | console.log(`Zone "${zone}" has been completely deleted`);
326 | } else {
327 | console.error(`Failed to delete zone "${zone}"`);
328 | process.exit(1);
329 | }
330 | }
331 | } else {
332 | // Delete all zones
333 | const zones = await kgClient.listMemoryZones();
334 |
335 | // Delete all indices
336 | const client = kgClient['client']; // Access the private client property
337 |
338 | for (const zone of zones) {
339 | if (zone.name === 'default') {
340 | // Clear default zone but don't delete it
341 | const indexName = `knowledge-graph@default`;
342 | try {
343 | await client.indices.delete({ index: indexName });
344 | console.log(`Deleted index: ${indexName}`);
345 | } catch (error) {
346 | console.error(`Error deleting index ${indexName}:`, error);
347 | }
348 | } else {
349 | // Delete non-default zones
350 | await kgClient.deleteMemoryZone(zone.name);
351 | console.log(`Deleted zone: ${zone.name}`);
352 | }
353 | }
354 |
355 | // Delete relations index
356 | try {
357 | await client.indices.delete({ index: KG_RELATIONS_INDEX });
358 | console.log('Deleted relations index');
359 | } catch (error) {
360 | console.error('Error deleting relations index:', error);
361 | }
362 |
363 | // Re-initialize everything
364 | await kgClient.initialize();
365 | console.log('Knowledge graph has been completely reset');
366 | }
367 | } else {
368 | console.log('Operation cancelled');
369 | }
370 |
371 | } catch (error) {
372 | console.error('Error resetting index:', (error as Error).message);
373 | process.exit(1);
374 | }
375 | }
376 |
377 | /**
378 | * Display information about a specific entity
379 | * @param name Entity name
380 | * @param zone Optional zone name
381 | */
382 | async function showEntity(name: string, zone?: string) {
383 | try {
384 | // Initialize client
385 | await kgClient.initialize(zone);
386 |
387 | // Get entity
388 | const entity = await kgClient.getEntityWithoutUpdatingLastRead(name, zone);
389 | if (!entity) {
390 | console.error(`Entity "${name}" not found${zone ? ` in zone "${zone}"` : ''}`);
391 | process.exit(1);
392 | }
393 |
394 | // Get related entities
395 | const related = await kgClient.getRelatedEntities(name, 1, zone);
396 |
397 | // Display entity information
398 | console.log(`Entity: ${entity.name}`);
399 | console.log(`Type: ${entity.entityType}`);
400 | console.log(`Zone: ${entity.zone || 'default'}`);
401 | console.log(`Last read: ${entity.lastRead}`);
402 | console.log(`Last write: ${entity.lastWrite}`);
403 | console.log(`Read count: ${entity.readCount}`);
404 | console.log(`Relevance score: ${typeof entity.relevanceScore === 'number' ? entity.relevanceScore.toFixed(2) : '1.00'} (higher = more important)`);
405 | console.log('');
406 |
407 | console.log('Observations:');
408 | entity.observations.forEach((obs: string, i: number) => {
409 | console.log(` ${i+1}. ${obs}`);
410 | });
411 | console.log('');
412 |
413 | console.log('Relations:');
414 | for (const relation of related.relations) {
415 | if (relation.from === name && relation.fromZone === (entity.zone || 'default')) {
416 | console.log(` → ${relation.relationType} → ${relation.to} [${relation.toZone}]`);
417 | } else {
418 | console.log(` ← ${relation.relationType} ← ${relation.from} [${relation.fromZone}]`);
419 | }
420 | }
421 | } catch (error) {
422 | console.error('Error getting entity:', (error as Error).message);
423 | process.exit(1);
424 | }
425 | }
426 |
427 | /**
428 | * List all memory zones
429 | */
430 | async function listZones() {
431 | try {
432 | await kgClient.initialize();
433 | const zones = await kgClient.listMemoryZones();
434 |
435 | console.log('Memory Zones:');
436 | console.log('=============');
437 |
438 | for (const zone of zones) {
439 | console.log(`${zone.name}`);
440 | console.log(` Description: ${zone.description || 'N/A'}`);
441 | console.log(` Created: ${zone.createdAt}`);
442 | console.log(` Last modified: ${zone.lastModified}`);
443 | console.log('');
444 | }
445 |
446 | console.log(`Total: ${zones.length} zones`);
447 | } catch (error) {
448 | console.error('Error listing zones:', (error as Error).message);
449 | process.exit(1);
450 | }
451 | }
452 |
453 | /**
454 | * Add a new memory zone
455 | */
456 | async function addZone(name: string, description?: string) {
457 | try {
458 | await kgClient.initialize();
459 | await kgClient.addMemoryZone(name, description);
460 | console.log(`Zone "${name}" created successfully`);
461 | } catch (error) {
462 | console.error('Error adding zone:', (error as Error).message);
463 | process.exit(1);
464 | }
465 | }
466 |
467 | /**
468 | * Delete a memory zone
469 | */
470 | async function deleteZone(name: string, args: string[] = []) {
471 | try {
472 | const confirmMessage = `Are you sure you want to delete zone "${name}" and all its data? This cannot be undone. (y/N) `;
473 | const confirmed = await confirmAction(confirmMessage, args);
474 |
475 | if (confirmed) {
476 | await kgClient.initialize();
477 | const success = await kgClient.deleteMemoryZone(name);
478 | if (success) {
479 | console.log(`Zone "${name}" deleted successfully`);
480 | } else {
481 | console.error(`Failed to delete zone "${name}"`);
482 | process.exit(1);
483 | }
484 | } else {
485 | console.log('Operation cancelled');
486 | }
487 | } catch (error) {
488 | console.error('Error deleting zone:', (error as Error).message);
489 | process.exit(1);
490 | }
491 | }
492 |
493 | /**
494 | * Show relations for a specific entity
495 | */
496 | async function showRelations(name: string, zone?: string) {
497 | try {
498 | await kgClient.initialize(zone);
499 |
500 | // Check if entity exists
501 | const entity = await kgClient.getEntityWithoutUpdatingLastRead(name, zone);
502 | if (!entity) {
503 | console.error(`Entity "${name}" not found${zone ? ` in zone "${zone}"` : ''}`);
504 | process.exit(1);
505 | }
506 |
507 | const actualZone = zone || 'default';
508 |
509 | // Get all relations for this entity
510 | const { relations } = await kgClient.getRelationsForEntities([name], actualZone);
511 |
512 | console.log(`Relations for entity "${name}" in zone "${actualZone}":"`);
513 | console.log('====================================');
514 |
515 | if (relations.length === 0) {
516 | console.log('No relations found.');
517 | return;
518 | }
519 |
520 | // Group by relation type
521 | const relationsByType = new Map<string, ESRelation[]>();
522 |
523 | for (const relation of relations) {
524 | if (!relationsByType.has(relation.relationType)) {
525 | relationsByType.set(relation.relationType, []);
526 | }
527 | relationsByType.get(relation.relationType)!.push(relation);
528 | }
529 |
530 | // Display grouped relations
531 | for (const [type, rels] of relationsByType.entries()) {
532 | console.log(`\n${type} (${rels.length}):`);
533 | console.log('----------------');
534 |
535 | for (const rel of rels) {
536 | if (rel.from === name && rel.fromZone === actualZone) {
537 | // This entity is the source
538 | console.log(`→ ${rel.to} [${rel.toZone}]`);
539 | } else {
540 | // This entity is the target
541 | console.log(`← ${rel.from} [${rel.fromZone}]`);
542 | }
543 | }
544 | }
545 | } catch (error) {
546 | console.error('Error showing relations:', (error as Error).message);
547 | process.exit(1);
548 | }
549 | }
550 |
551 | /**
552 | * Backup all zones to a file
553 | */
554 | async function backupAll(filePath: string) {
555 | try {
556 | await kgClient.initialize();
557 |
558 | // Export all data from all zones
559 | console.log('Exporting all zones and relations...');
560 | const data = await kgClient.exportAllData();
561 |
562 | console.log(`Found ${data.entities.length} entities, ${data.relations.length} relations, and ${data.zones.length} zones`);
563 |
564 | // Write to file
565 | const jsonData = JSON.stringify(data, null, 2);
566 | await fs.writeFile(filePath, jsonData);
567 |
568 | console.log(`Backup saved to ${filePath}`);
569 | console.log(`Entities: ${data.entities.length}`);
570 | console.log(`Relations: ${data.relations.length}`);
571 | console.log(`Zones: ${data.zones.length}`);
572 | } catch (error) {
573 | console.error('Error creating backup:', (error as Error).message);
574 | process.exit(1);
575 | }
576 | }
577 |
578 | /**
579 | * Restore all zones from a backup file
580 | */
581 | async function restoreAll(filePath: string, args: string[] = []) {
582 | try {
583 | // Read the backup file
584 | const jsonData = await fs.readFile(filePath, 'utf8');
585 | const data = JSON.parse(jsonData);
586 |
587 | if (!data.entities || !data.relations || !data.zones) {
588 | console.error('Invalid backup file format');
589 | process.exit(1);
590 | }
591 |
592 | console.log(`Found ${data.entities.length} entities, ${data.relations.length} relations, and ${data.zones.length} zones in backup`);
593 |
594 | // Confirm with user
595 | const confirmMessage = 'This will merge the backup with existing data. Continue? (y/N) ';
596 | const confirmed = await confirmAction(confirmMessage, args);
597 |
598 | if (!confirmed) {
599 | console.log('Operation cancelled');
600 | return;
601 | }
602 |
603 | // Import the data
604 | await kgClient.initialize();
605 | const result = await kgClient.importAllData(data);
606 |
607 | console.log('Restore completed:');
608 | console.log(`Zones added: ${result.zonesAdded}`);
609 | console.log(`Entities added: ${result.entitiesAdded}`);
610 | console.log(`Relations added: ${result.relationsAdded}`);
611 | } catch (error) {
612 | console.error('Error restoring backup:', (error as Error).message);
613 | process.exit(1);
614 | }
615 | }
616 |
617 | /**
618 | * Helper function to check if --yes flag is present
619 | */
620 | function hasYesFlag(args: string[]): boolean {
621 | return args.includes('--yes') || args.includes('-y');
622 | }
623 |
624 | /**
625 | * Remove --yes or -y flags from arguments if present
626 | */
627 | function cleanArgs(args: string[]): string[] {
628 | return args.filter(arg => arg !== '--yes' && arg !== '-y');
629 | }
630 |
631 | /**
632 | * Confirm an action with the user
633 | * @param message The confirmation message to display
634 | * @param args Command line arguments to check for --yes flag
635 | */
636 | async function confirmAction(message: string, args: string[]): Promise<boolean> {
637 | // Skip confirmation if --yes flag is present
638 | if (hasYesFlag(args)) {
639 | return true;
640 | }
641 |
642 | // Otherwise, ask for confirmation
643 | const rl = readline.createInterface({
644 | input: process.stdin,
645 | output: process.stdout
646 | });
647 |
648 | const answer = await new Promise<string>((resolve) => {
649 | rl.question(message, (ans: string) => {
650 | resolve(ans);
651 | rl.close();
652 | });
653 | });
654 |
655 | return answer.toLowerCase() === 'y';
656 | }
657 |
658 | /**
659 | * Update zone descriptions based on content
660 | * @param zone Zone name to update
661 | * @param limit Optional maximum number of entities to analyze
662 | * @param userPrompt Optional user-provided description of the zone's purpose
663 | */
664 | async function updateZoneDescriptions(zone: string, limit: number = 20, userPrompt?: string) {
665 | try {
666 | console.log(`Updating descriptions for zone "${zone}" based on content...`);
667 |
668 | // Initialize client
669 | await kgClient.initialize(zone);
670 |
671 | // Get current zone metadata
672 | const zoneMetadata = await kgClient.getZoneMetadata(zone);
673 | if (!zoneMetadata) {
674 | console.error(`Zone "${zone}" not found`);
675 | process.exit(1);
676 | }
677 |
678 | console.log(`Finding the most representative entities to answer "What is ${zone}?"...`);
679 |
680 | // Try multiple search strategies to get the most representative entities
681 | const relevantEntities = [];
682 |
683 | // If user provided a prompt, search for it specifically
684 | if (userPrompt) {
685 | console.log(`Using user-provided description: "${userPrompt}"`);
686 |
687 | const { entities: promptEntities } = await kgClient.userSearch({
688 | query: userPrompt,
689 | limit: limit,
690 | sortBy: 'importance',
691 | includeObservations: true,
692 | informationNeeded: zone !== 'default' ? `What is ${zone}?` : undefined,
693 | reason: `Trying to figure out what ${zone} is about, in order to update the zone description.`,
694 | zone: zone
695 | });
696 |
697 | relevantEntities.push(...promptEntities);
698 | }
699 |
700 | // Strategy 1: First get most important entities
701 | if (relevantEntities.length < limit) {
702 | const { entities: importantEntities } = await kgClient.userSearch({
703 | query: "*", // Get all entities
704 | limit: Math.floor(limit / 2),
705 | sortBy: 'importance',
706 | includeObservations: true,
707 | informationNeeded: zone !== 'default' ? `What is ${zone}?` : undefined,
708 | reason: `Trying to figure out what ${zone} is about, in order to update the zone description.`,
709 | zone: zone
710 | });
711 |
712 | // Add only new entities
713 | for (const entity of importantEntities) {
714 | if (!relevantEntities.some(e =>
715 | e.entityType && // Make sure we're comparing entities
716 | entity.entityType &&
717 | e.name === entity.name
718 | )) {
719 | relevantEntities.push(entity);
720 | }
721 | }
722 | }
723 |
724 | // Strategy 2: Use zone name as search query to find semantically related entities
725 | if (relevantEntities.length < limit) {
726 | const { entities: nameEntities } = await kgClient.userSearch({
727 | query: zone, // Use zone name as search query
728 | limit: Math.ceil(limit / 4),
729 | sortBy: 'relevance',
730 | includeObservations: true,
731 | informationNeeded: zone !== 'default' ? `What is ${zone}?` : undefined,
732 | reason: `Trying to figure out what ${zone} is about, in order to update the zone description.`,
733 | zone: zone
734 | });
735 |
736 | // Add only new entities not already in the list
737 | for (const entity of nameEntities) {
738 | if (!relevantEntities.some(e =>
739 | e.entityType && // Make sure we're comparing entities
740 | entity.entityType &&
741 | e.name === entity.name
742 | )) {
743 | relevantEntities.push(entity);
744 | }
745 | }
746 | }
747 |
748 | // Strategy 3: Get most frequently accessed entities
749 | if (relevantEntities.length < limit) {
750 | const { entities: recentEntities } = await kgClient.userSearch({
751 | query: "*", // Get all entities
752 | limit: Math.ceil(limit / 4),
753 | sortBy: 'recent',
754 | includeObservations: true,
755 | informationNeeded: zone !== 'default' ? `What is ${zone}?` : undefined,
756 | reason: `Trying to figure out what ${zone} is about, in order to update the zone description.`,
757 | zone: zone
758 | });
759 |
760 | // Add only new entities not already in the list
761 | for (const entity of recentEntities) {
762 | if (!relevantEntities.some(e =>
763 | e.entityType && // Make sure we're comparing entities
764 | entity.entityType &&
765 | e.name === entity.name
766 | )) {
767 | relevantEntities.push(entity);
768 | }
769 | }
770 | }
771 |
772 | if (relevantEntities.length === 0) {
773 | console.log(`No entities found in zone "${zone}" to analyze.`);
774 | return;
775 | }
776 |
777 | // Trim to limit
778 | const finalEntities = relevantEntities.slice(0, limit);
779 |
780 | console.log(`Found ${finalEntities.length} representative entities to analyze for zone description.`);
781 |
782 | // Generate descriptions using AI
783 | console.log("\nGenerating descriptions...");
784 | try {
785 | const descriptions = await GroqAI.generateZoneDescriptions(
786 | zone,
787 | zoneMetadata.description || '',
788 | finalEntities,
789 | userPrompt
790 | );
791 |
792 | // Update the zone with new descriptions
793 | await kgClient.updateZoneDescriptions(
794 | zone,
795 | descriptions.description,
796 | descriptions.shortDescription
797 | );
798 |
799 | console.log(`\nUpdated descriptions for zone "${zone}":`);
800 | console.log(`\nShort Description: ${descriptions.shortDescription}`);
801 | console.log(`\nFull Description: ${descriptions.description}`);
802 | } catch (error) {
803 | console.error(`\nError generating descriptions: ${error.message}`);
804 | console.log('\nFalling back to existing description. Please try again or provide a more specific prompt.');
805 | }
806 |
807 | } catch (error) {
808 | console.error('Error updating zone descriptions:', (error as Error).message);
809 | process.exit(1);
810 | }
811 | }
812 |
813 | /**
814 | * Main function to parse and execute commands
815 | */
816 | async function main() {
817 | const args = process.argv.slice(2);
818 | const cleanedArgs = cleanArgs(args);
819 | const command = cleanedArgs[0];
820 |
821 | if (!command || command === 'help') {
822 | showHelp();
823 | return;
824 | }
825 |
826 | switch (command) {
827 | case 'init':
828 | await initializeIndex();
829 | break;
830 |
831 | case 'import':
832 | if (!cleanedArgs[1]) {
833 | console.error('Error: File path is required for import');
834 | process.exit(1);
835 | }
836 | await importFromJsonFile(cleanedArgs[1], {
837 | ...esOptions,
838 | defaultZone: cleanedArgs[2] || DEFAULT_ZONE
839 | });
840 | break;
841 |
842 | case 'export':
843 | if (!cleanedArgs[1]) {
844 | console.error('Error: File path is required for export');
845 | process.exit(1);
846 | }
847 | await exportToJsonFile(cleanedArgs[1], {
848 | ...esOptions,
849 | defaultZone: cleanedArgs[2] || DEFAULT_ZONE
850 | });
851 | break;
852 |
853 | case 'backup':
854 | if (!cleanedArgs[1]) {
855 | console.error('Error: File path is required for backup');
856 | process.exit(1);
857 | }
858 | await backupAll(cleanedArgs[1]);
859 | break;
860 |
861 | case 'restore':
862 | if (!cleanedArgs[1]) {
863 | console.error('Error: File path is required for restore');
864 | process.exit(1);
865 | }
866 | await restoreAll(cleanedArgs[1], args);
867 | break;
868 |
869 | case 'stats':
870 | await showStats(cleanedArgs[1]);
871 | break;
872 |
873 | case 'search':
874 | if (!cleanedArgs[1]) {
875 | console.error('Error: Search query is required');
876 | process.exit(1);
877 | }
878 | await searchGraph(cleanedArgs[1], cleanedArgs[2]);
879 | break;
880 |
881 | case 'reset':
882 | await resetIndex(cleanedArgs[1], args);
883 | break;
884 |
885 | case 'entity':
886 | if (!cleanedArgs[1]) {
887 | console.error('Error: Entity name is required');
888 | process.exit(1);
889 | }
890 | await showEntity(cleanedArgs[1], cleanedArgs[2]);
891 | break;
892 |
893 | case 'zones':
894 | const zonesCommand = cleanedArgs[1];
895 |
896 | if (!zonesCommand) {
897 | await listZones();
898 | break;
899 | }
900 |
901 | switch (zonesCommand) {
902 | case 'list':
903 | await listZones();
904 | break;
905 |
906 | case 'add':
907 | if (!cleanedArgs[2]) {
908 | console.error('Error: Zone name is required');
909 | process.exit(1);
910 | }
911 | await addZone(cleanedArgs[2], cleanedArgs[3]);
912 | break;
913 |
914 | case 'delete':
915 | if (!cleanedArgs[2]) {
916 | console.error('Error: Zone name is required');
917 | process.exit(1);
918 | }
919 | await deleteZone(cleanedArgs[2], args);
920 | break;
921 |
922 | case 'stats':
923 | if (!cleanedArgs[2]) {
924 | console.error('Error: Zone name is required');
925 | process.exit(1);
926 | }
927 | await showStats(cleanedArgs[2]);
928 | break;
929 |
930 | case 'update_descriptions':
931 | if (!cleanedArgs[2]) {
932 | console.error('Error: Zone name is required');
933 | process.exit(1);
934 | }
935 |
936 | let limit = 20;
937 | let userPrompt = undefined;
938 |
939 | // Check if the third argument is a number (limit) or a string (userPrompt)
940 | if (cleanedArgs[3]) {
941 | const parsedLimit = parseInt(cleanedArgs[3], 10);
942 | if (!isNaN(parsedLimit) && parsedLimit.toString() === cleanedArgs[3]) {
943 | // Only interpret as a limit if it's a pure number with no text
944 | limit = parsedLimit;
945 | // If there's a fourth argument, it's the user prompt
946 | if (cleanedArgs[4]) {
947 | userPrompt = cleanedArgs.slice(4).join(' ');
948 | }
949 | } else {
950 | // If third argument isn't a pure number, it's the start of the user prompt
951 | userPrompt = cleanedArgs.slice(3).join(' ');
952 | }
953 | }
954 |
955 | await updateZoneDescriptions(cleanedArgs[2], limit, userPrompt);
956 | break;
957 |
958 | default:
959 | console.error(`Unknown zones command: ${zonesCommand}`);
960 | showHelp();
961 | process.exit(1);
962 | }
963 | break;
964 |
965 | case 'relations':
966 | if (!cleanedArgs[1]) {
967 | console.error('Error: Entity name is required');
968 | process.exit(1);
969 | }
970 | await showRelations(cleanedArgs[1], cleanedArgs[2]);
971 | break;
972 |
973 | default:
974 | console.error(`Unknown command: ${command}`);
975 | showHelp();
976 | process.exit(1);
977 | }
978 | }
979 |
980 | // Run the CLI
981 | main().catch(error => {
982 | console.error('Error:', (error as Error).message);
983 | process.exit(1);
984 | });
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | // @ts-ignore
4 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5 | // @ts-ignore
6 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7 | import {
8 | CallToolRequestSchema,
9 | ListToolsRequestSchema,
10 | ListResourcesRequestSchema,
11 | ListPromptsRequestSchema
12 | } from "@modelcontextprotocol/sdk/types.js";
13 | import { KnowledgeGraphClient } from './kg-client.js';
14 | import { ESEntity, ESRelation, ESSearchParams } from './es-types.js';
15 | import GroqAI from './ai-service.js';
16 | import { inspectFile } from './filesystem/index.js';
17 |
18 | // Environment configuration for Elasticsearch
19 | const ES_NODE = process.env.ES_NODE || 'http://localhost:9200';
20 | const ES_USERNAME = process.env.ES_USERNAME;
21 | const ES_PASSWORD = process.env.ES_PASSWORD;
22 | const DEBUG = process.env.DEBUG === 'true';
23 |
24 | // Configure ES client with authentication if provided
25 | const esOptions: { node: string; auth?: { username: string; password: string } } = {
26 | node: ES_NODE
27 | };
28 |
29 | if (ES_USERNAME && ES_PASSWORD) {
30 | esOptions.auth = { username: ES_USERNAME, password: ES_PASSWORD };
31 | }
32 |
33 | // Create KG client
34 | const kgClient = new KnowledgeGraphClient(esOptions);
35 |
36 | // Helper function to format dates in YYYY-MM-DD format
37 | function formatDate(date: Date = new Date()): string {
38 | return date.toISOString().split('T')[0]; // Returns YYYY-MM-DD
39 | }
40 |
41 | // Start the MCP server
42 | async function startServer() {
43 | try {
44 | // Initialize the knowledge graph
45 | await kgClient.initialize();
46 | // Use stderr for logging, not stdout
47 | console.error('Elasticsearch Knowledge Graph initialized');
48 | } catch (error) {
49 | console.error('Warning: Failed to connect to Elasticsearch:', error.message);
50 | console.error('The memory server will still start, but operations requiring Elasticsearch will fail');
51 | }
52 |
53 | // Create the MCP server
54 | const server = new Server({
55 | name: "memory",
56 | version: "1.0.0",
57 | }, {
58 | capabilities: {
59 | tools: {},
60 | // Add empty resource and prompt capabilities to support list requests
61 | resources: {},
62 | prompts: {}
63 | },
64 | });
65 |
66 | console.error('Starting MCP server...');
67 |
68 | // Handle resources/list requests (return empty list)
69 | server.setRequestHandler(ListResourcesRequestSchema, async () => {
70 | if (DEBUG) {
71 | console.error('ListResourcesRequestSchema');
72 | }
73 | return {
74 | resources: []
75 | };
76 | });
77 |
78 | // Handle prompts/list requests (return empty list)
79 | server.setRequestHandler(ListPromptsRequestSchema, async () => {
80 | if (DEBUG) {
81 | console.error('ListPromptsRequestSchema');
82 | }
83 | return {
84 | prompts: []
85 | };
86 | });
87 |
88 | // Register the tools handler to list all available tools
89 | server.setRequestHandler(ListToolsRequestSchema, async () => {
90 | return {
91 | tools: [
92 | {
93 | name: "inspect_files",
94 | description: "Agent driven file inspection that uses AI to retrieve relevant content from multiple files.",
95 | inputSchema: {
96 | type: "object",
97 | properties: {
98 | file_paths: {
99 | type: "array",
100 | items: { type: "string" },
101 | description: "Paths to the files (or directories) to inspect"
102 | },
103 | information_needed: {
104 | type: "string",
105 | description: "Full description of what information is needed from the files, including the context of the information needed. Do not be vague, be specific. The AI agent does not have access to your context, only this \"information needed\" and \"reason\" fields. That's all it will use to decide that a line is relevant to the information needed. So provide a detailed specific description, listing all the details about what you are looking for."
106 | },
107 | reason: {
108 | type: "string",
109 | description: "Explain why this information is needed to help the AI agent give better results. The more context you provide, the better the results will be."
110 | },
111 | include_lines: {
112 | type: "boolean",
113 | description: "Whether to include the actual line content in the response, which uses more of your limited token quota, but gives more informatiom (default: false)"
114 | },
115 | keywords: {
116 | type: "array",
117 | items: { type: "string" },
118 | description: "Array of specific keywords related to the information needed. AI will target files that contain one of these keywords. REQUIRED and cannot be null or empty - the more keywords you provide, the better the results. Include variations, synonyms, and related terms."
119 | }
120 | },
121 | required: ["file_paths", "information_needed", "include_lines", "keywords"],
122 | additionalProperties: false,
123 | "$schema": "http://json-schema.org/draft-07/schema#"
124 | }
125 | },
126 | {
127 | name: "inspect_knowledge_graph",
128 | description: "Agent driven knowledge graph inspection that uses AI to retrieve relevant entities and relations based on a query.",
129 | inputSchema: {
130 | type: "object",
131 | properties: {
132 | information_needed: {
133 | type: "string",
134 | description: "Full description of what information is needed from the knowledge graph, including the context of the information needed. Do not be vague, be specific. The AI agent does not have access to your context, only this \"information needed\" and \"reason\" fields. That's all it will use to decide that an entity is relevant to the information needed."
135 | },
136 | reason: {
137 | type: "string",
138 | description: "Explain why this information is needed to help the AI agent give better results. The more context you provide, the better the results will be."
139 | },
140 | include_entities: {
141 | type: "boolean",
142 | description: "Whether to include the full entity details in the response, which uses more of your limited token quota, but gives more information (default: false)"
143 | },
144 | include_relations: {
145 | type: "boolean",
146 | description: "Whether to include the entity relations in the response (default: false)"
147 | },
148 | keywords: {
149 | type: "array",
150 | items: { type: "string" },
151 | description: "Array of specific keywords related to the information needed. AI will target entities that match one of these keywords. REQUIRED and cannot be null or empty - the more keywords you provide, the better the results. Include variations, synonyms, and related terms."
152 | },
153 | memory_zone: {
154 | type: "string",
155 | description: "Memory zone to search in. If not provided, uses the default zone."
156 | },
157 | entity_types: {
158 | type: "array",
159 | items: { type: "string" },
160 | description: "Optional filter to specific entity types"
161 | }
162 | },
163 | required: ["information_needed", "keywords"],
164 | additionalProperties: false,
165 | "$schema": "http://json-schema.org/draft-07/schema#"
166 | }
167 | },
168 | {
169 | name: "create_entities",
170 | description: "Create entities in knowledge graph (memory)",
171 | inputSchema: {
172 | type: "object",
173 | properties: {
174 | entities: {
175 | type: "array",
176 | items: {
177 | type: "object",
178 | properties: {
179 | name: {type: "string", description: "Entity name"},
180 | entityType: {type: "string", description: "Entity type"},
181 | observations: {
182 | type: "array",
183 | items: {type: "string"},
184 | description: "Observations about this entity"
185 | }
186 | },
187 | required: ["name", "entityType"]
188 | },
189 | description: "List of entities to create"
190 | },
191 | memory_zone: {
192 | type: "string",
193 | description: "Memory zone to create entities in."
194 | }
195 | },
196 | required: ["entities", "memory_zone"],
197 | additionalProperties: false,
198 | "$schema": "http://json-schema.org/draft-07/schema#"
199 | }
200 | },
201 | {
202 | name: "update_entities",
203 | description: "Update entities in knowledge graph (memory)",
204 | inputSchema: {
205 | type: "object",
206 | properties: {
207 | entities: {
208 | type: "array",
209 | description: "List of entities to update",
210 | items: {
211 | type: "object",
212 | properties: {
213 | name: {type: "string"},
214 | entityType: {type: "string"},
215 | observations: {
216 | type: "array",
217 | items: {type: "string"}
218 | },
219 | isImportant: {type: "boolean"}
220 | },
221 | required: ["name"]
222 | }
223 | },
224 | memory_zone: {
225 | type: "string",
226 | description: "Memory zone specifier. Entities will be updated in this zone."
227 | }
228 | },
229 | required: ["entities", "memory_zone"],
230 | additionalProperties: false,
231 | "$schema": "http://json-schema.org/draft-07/schema#"
232 | }
233 | },
234 | {
235 | name: "delete_entities",
236 | description: "Delete entities from knowledge graph (memory)",
237 | inputSchema: {
238 | type: "object",
239 | properties: {
240 | names: {
241 | type: "array",
242 | items: {type: "string"},
243 | description: "Names of entities to delete"
244 | },
245 | memory_zone: {
246 | type: "string",
247 | description: "Memory zone specifier. Entities will be deleted from this zone."
248 | },
249 | cascade_relations: {
250 | type: "boolean",
251 | description: "Whether to delete relations involving these entities (default: true)",
252 | default: true
253 | }
254 | },
255 | required: ["names", "memory_zone"],
256 | additionalProperties: false,
257 | "$schema": "http://json-schema.org/draft-07/schema#"
258 | }
259 | },
260 | {
261 | name: "create_relations",
262 | description: "Create relationships between entities in knowledge graph (memory)",
263 | inputSchema: {
264 | type: "object",
265 | properties: {
266 | relations: {
267 | type: "array",
268 | description: "List of relations to create",
269 | items: {
270 | type: "object",
271 | properties: {
272 | from: {type: "string", description: "Source entity name"},
273 | fromZone: {type: "string", description: "Optional zone for source entity, defaults to memory_zone or default zone. Must be one of the existing zones."},
274 | to: {type: "string", description: "Target entity name"},
275 | toZone: {type: "string", description: "Optional zone for target entity, defaults to memory_zone or default zone. Must be one of the existing zones."},
276 | type: {type: "string", description: "Relationship type"}
277 | },
278 | required: ["from", "to", "type"]
279 | }
280 | },
281 | memory_zone: {
282 | type: "string",
283 | description: "Optional default memory zone specifier. Used if a relation doesn't specify fromZone or toZone."
284 | },
285 | auto_create_missing_entities: {
286 | type: "boolean",
287 | description: "Whether to automatically create missing entities in the relations (default: true)",
288 | default: true
289 | }
290 | },
291 | required: ["relations"],
292 | additionalProperties: false,
293 | "$schema": "http://json-schema.org/draft-07/schema#"
294 | }
295 | },
296 | {
297 | name: "delete_relations",
298 | description: "Delete relationships from knowledge graph (memory)",
299 | inputSchema: {
300 | type: "object",
301 | properties: {
302 | relations: {
303 | type: "array",
304 | description: "List of relations to delete",
305 | items: {
306 | type: "object",
307 | properties: {
308 | from: {type: "string", description: "Source entity name"},
309 | to: {type: "string", description: "Target entity name"},
310 | type: {type: "string", description: "Relationship type"}
311 | },
312 | required: ["from", "to", "type"]
313 | }
314 | },
315 | memory_zone: {
316 | type: "string",
317 | description: "Optional memory zone specifier. If provided, relations will be deleted from this zone."
318 | }
319 | },
320 | required: ["relations"],
321 | additionalProperties: false,
322 | "$schema": "http://json-schema.org/draft-07/schema#"
323 | }
324 | },
325 | {
326 | name: "search_nodes",
327 | description: "Search entities using ElasticSearch query syntax. Supports boolean operators (AND, OR, NOT), fuzzy matching (~), phrases (\"term\"), proximity (\"terms\"~N), wildcards (*, ?), and boosting (^N). Examples: 'meeting AND notes', 'Jon~', '\"project plan\"~2'. All searches respect zone isolation.",
328 | inputSchema: {
329 | type: "object",
330 | properties: {
331 | query: {
332 | type: "string",
333 | description: "ElasticSearch query string."
334 | },
335 | informationNeeded: {
336 | type: "string",
337 | description: "Important. Describe what information you are looking for, to give a precise context to the search engine AI agent. What questions are you trying to answer? Helps get more useful results."
338 | },
339 | reason: {
340 | type: "string",
341 | description: "Explain why this information is needed to help the AI agent give better results. The more context you provide, the better the results will be."
342 | },
343 | entityTypes: {
344 | type: "array",
345 | items: {type: "string"},
346 | description: "Filter to specific entity types (OR condition if multiple)."
347 | },
348 | limit: {
349 | type: "integer",
350 | description: "Max results (default: 20, or 5 with observations)."
351 | },
352 | sortBy: {
353 | type: "string",
354 | enum: ["relevance", "recency", "importance"],
355 | description: "Sort by match quality, access time, or importance."
356 | },
357 | includeObservations: {
358 | type: "boolean",
359 | description: "Include full entity observations (default: false).",
360 | default: false
361 | },
362 | memory_zone: {
363 | type: "string",
364 | description: "Limit search to specific zone. Omit for default zone."
365 | },
366 | },
367 | required: ["query", "memory_zone", "informationNeeded", "reason"],
368 | additionalProperties: false,
369 | "$schema": "http://json-schema.org/draft-07/schema#"
370 | }
371 | },
372 | {
373 | name: "open_nodes",
374 | description: "Get details about specific entities in knowledge graph (memory) and their relations",
375 | inputSchema: {
376 | type: "object",
377 | properties: {
378 | names: {
379 | type: "array",
380 | items: {type: "string"},
381 | description: "Names of entities to retrieve"
382 | },
383 | memory_zone: {
384 | type: "string",
385 | description: "Optional memory zone to retrieve entities from. If not specified, uses the default zone."
386 | }
387 | },
388 | required: ["names", "memory_zone"],
389 | additionalProperties: false,
390 | "$schema": "http://json-schema.org/draft-07/schema#"
391 | }
392 | },
393 | {
394 | name: "add_observations",
395 | description: "Add observations to an existing entity in knowledge graph (memory)",
396 | inputSchema: {
397 | type: "object",
398 | properties: {
399 | name: {
400 | type: "string",
401 | description: "Name of entity to add observations to"
402 | },
403 | observations: {
404 | type: "array",
405 | items: {type: "string"},
406 | description: "Observations to add to the entity"
407 | },
408 | memory_zone: {
409 | type: "string",
410 | description: "Optional memory zone where the entity is stored. If not specified, uses the default zone."
411 | }
412 | },
413 | required: ["memory_zone", "name", "observations"],
414 | additionalProperties: false,
415 | "$schema": "http://json-schema.org/draft-07/schema#"
416 | }
417 | },
418 | {
419 | name: "mark_important",
420 | description: "Mark entity as important in knowledge graph (memory) by boosting its relevance score",
421 | inputSchema: {
422 | type: "object",
423 | properties: {
424 | name: {
425 | type: "string",
426 | description: "Entity name"
427 | },
428 | important: {
429 | type: "boolean",
430 | description: "Set as important (true - multiply relevance by 10) or not (false - divide relevance by 10)"
431 | },
432 | memory_zone: {
433 | type: "string",
434 | description: "Optional memory zone specifier. If provided, entity will be marked in this zone."
435 | },
436 | auto_create: {
437 | type: "boolean",
438 | description: "Whether to automatically create the entity if it doesn't exist (default: false)",
439 | default: false
440 | }
441 | },
442 | required: ["memory_zone", "name", "important"],
443 | additionalProperties: false,
444 | "$schema": "http://json-schema.org/draft-07/schema#"
445 | }
446 | },
447 | {
448 | name: "get_recent",
449 | description: "Get recently accessed entities from knowledge graph (memory) and their relations",
450 | inputSchema: {
451 | type: "object",
452 | properties: {
453 | limit: {
454 | type: "integer",
455 | description: "Max results (default: 20 if includeObservations is false, 5 if true)"
456 | },
457 | includeObservations: {
458 | type: "boolean",
459 | description: "Whether to include full entity observations in results (default: false)",
460 | default: false
461 | },
462 | memory_zone: {
463 | type: "string",
464 | description: "Optional memory zone to get recent entities from. If not specified, uses the default zone."
465 | }
466 | },
467 | required: ["memory_zone"],
468 | additionalProperties: false,
469 | "$schema": "http://json-schema.org/draft-07/schema#"
470 | }
471 | },
472 | {
473 | name: "list_zones",
474 | description: "List all available memory zones with metadata. When a reason is provided, zones will be filtered and prioritized based on relevance to your needs.",
475 | inputSchema: {
476 | type: "object",
477 | properties: {
478 | reason: {
479 | type: "string",
480 | description: "Reason for listing zones. What zones are you looking for? Why are you looking for them? The AI will use this to prioritize and filter relevant zones."
481 | }
482 | },
483 | additionalProperties: false,
484 | "$schema": "http://json-schema.org/draft-07/schema#"
485 | }
486 | },
487 | {
488 | name: "create_zone",
489 | description: "Create a new memory zone with optional description.",
490 | inputSchema: {
491 | type: "object",
492 | properties: {
493 | name: {
494 | type: "string",
495 | description: "Zone name (cannot be 'default')"
496 | },
497 | shortDescription: {
498 | type: "string",
499 | description: "Short description of the zone."
500 | },
501 | description: {
502 | type: "string",
503 | description: "Full zone description. Make it very descriptive and detailed."
504 | }
505 | },
506 | required: ["name"]
507 | }
508 | },
509 | {
510 | name: "delete_zone",
511 | description: "Delete a memory zone and all its entities/relations.",
512 | inputSchema: {
513 | type: "object",
514 | properties: {
515 | name: {
516 | type: "string",
517 | description: "Zone name to delete (cannot be 'default')"
518 | },
519 | confirm: {
520 | type: "boolean",
521 | description: "Confirmation flag, must be true",
522 | default: false
523 | }
524 | },
525 | required: ["name", "confirm"]
526 | }
527 | },
528 | {
529 | name: "copy_entities",
530 | description: "Copy entities between zones with optional relation handling.",
531 | inputSchema: {
532 | type: "object",
533 | properties: {
534 | names: {
535 | type: "array",
536 | items: { type: "string" },
537 | description: "Entity names to copy"
538 | },
539 | source_zone: {
540 | type: "string",
541 | description: "Source zone"
542 | },
543 | target_zone: {
544 | type: "string",
545 | description: "Target zone"
546 | },
547 | copy_relations: {
548 | type: "boolean",
549 | description: "Copy related relationships (default: true)",
550 | default: true
551 | },
552 | overwrite: {
553 | type: "boolean",
554 | description: "Overwrite if entity exists (default: false)",
555 | default: false
556 | }
557 | },
558 | required: ["names", "source_zone", "target_zone"]
559 | }
560 | },
561 | {
562 | name: "move_entities",
563 | description: "Move entities between zones (copy + delete from source).",
564 | inputSchema: {
565 | type: "object",
566 | properties: {
567 | names: {
568 | type: "array",
569 | items: { type: "string" },
570 | description: "Entity names to move"
571 | },
572 | source_zone: {
573 | type: "string",
574 | description: "Source zone"
575 | },
576 | target_zone: {
577 | type: "string",
578 | description: "Target zone"
579 | },
580 | move_relations: {
581 | type: "boolean",
582 | description: "Move related relationships (default: true)",
583 | default: true
584 | },
585 | overwrite: {
586 | type: "boolean",
587 | description: "Overwrite if entity exists (default: false)",
588 | default: false
589 | }
590 | },
591 | required: ["names", "source_zone", "target_zone"]
592 | }
593 | },
594 | {
595 | name: "merge_zones",
596 | description: "Merge multiple zones with conflict resolution options.",
597 | inputSchema: {
598 | type: "object",
599 | properties: {
600 | source_zones: {
601 | type: "array",
602 | items: { type: "string" },
603 | description: "Source zones to merge from"
604 | },
605 | target_zone: {
606 | type: "string",
607 | description: "Target zone to merge into"
608 | },
609 | delete_source_zones: {
610 | type: "boolean",
611 | description: "Delete source zones after merging",
612 | default: false
613 | },
614 | overwrite_conflicts: {
615 | type: "string",
616 | enum: ["skip", "overwrite", "rename"],
617 | description: "How to handle name conflicts",
618 | default: "skip"
619 | }
620 | },
621 | required: ["source_zones", "target_zone"]
622 | }
623 | },
624 | {
625 | name: "zone_stats",
626 | description: "Get statistics for entities and relationships in a zone.",
627 | inputSchema: {
628 | type: "object",
629 | properties: {
630 | zone: {
631 | type: "string",
632 | description: "Zone name (omit for default zone)"
633 | }
634 | },
635 | required: ["zone"]
636 | }
637 | },
638 | {
639 | name: "get_time_utc",
640 | description: "Get the current UTC time in YYYY-MM-DD hh:mm:ss format",
641 | inputSchema: {
642 | type: "object",
643 | properties: {},
644 | additionalProperties: false
645 | }
646 | }
647 | ]
648 | };
649 | });
650 |
651 | // Register the call tool handler to handle tool executions
652 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
653 | if (DEBUG) {
654 | console.error('Tool request received:', request.params.name);
655 | console.error('Tool request params:', JSON.stringify(request.params));
656 | }
657 |
658 | const toolName = request.params.name;
659 | const params = request.params.arguments as any;
660 |
661 | if (DEBUG) {
662 | console.error('Parsed parameters:', JSON.stringify(params));
663 | }
664 |
665 | // Helper function to format response for Claude
666 | const formatResponse = (data: any) => {
667 | const stringifiedData = JSON.stringify(data, null, 2);
668 | return {
669 | content: [
670 | {
671 | type: "text",
672 | text: stringifiedData,
673 | },
674 | ],
675 | };
676 | };
677 |
678 | if (toolName === "inspect_files") {
679 | const { file_paths, information_needed, reason, include_lines, keywords } = params;
680 |
681 | // Validate keywords
682 | if (!keywords || !Array.isArray(keywords) || keywords.length === 0) {
683 | return formatResponse({
684 | success: false,
685 | error: "Keywords parameter is required and cannot be null or empty. Please provide an array of keywords to help find relevant information. The more keywords you provide (including variations, synonyms, and related terms), the better the results."
686 | });
687 | }
688 |
689 | const results = [];
690 |
691 | for (const filePath of file_paths) {
692 | try {
693 | const fileResults = await inspectFile(filePath, information_needed, reason, keywords);
694 | results.push({
695 | filePath,
696 | linesContent: `lines as returned by cat -n ${filePath}`,
697 | lines: include_lines ? fileResults.lines.map(line => `${line.lineNumber}\t${line.content}`) : [],
698 | tentativeAnswer: fileResults.tentativeAnswer
699 | });
700 | } catch (error) {
701 | results.push({
702 | filePath,
703 | error: error.message
704 | });
705 | }
706 | }
707 |
708 | return formatResponse({
709 | success: true,
710 | results
711 | });
712 | }
713 | else if (toolName === "inspect_knowledge_graph") {
714 | const { information_needed, reason, include_entities, include_relations, keywords, memory_zone, entity_types } = params;
715 |
716 | // Validate keywords
717 | if (!keywords || !Array.isArray(keywords) || keywords.length === 0) {
718 | return formatResponse({
719 | success: false,
720 | error: "Keywords parameter is required and cannot be null or empty. Please provide an array of keywords to help find relevant entities. The more keywords you provide (including variations, synonyms, and related terms), the better the results."
721 | });
722 | }
723 |
724 | // Import the inspectKnowledgeGraph function
725 | const { inspectKnowledgeGraph } = await import('./kg-inspection.js');
726 |
727 | try {
728 | // Call the inspectKnowledgeGraph function
729 | const results = await inspectKnowledgeGraph(
730 | kgClient,
731 | information_needed,
732 | reason,
733 | keywords,
734 | memory_zone,
735 | entity_types
736 | );
737 |
738 | // Format the response based on include_entities and include_relations flags
739 | return formatResponse({
740 | success: true,
741 | tentativeAnswer: results.tentativeAnswer,
742 | entities: include_entities ? results.entities : results.entities.map(e => ({ name: e.name, entityType: e.entityType })),
743 | relations: include_relations ? results.relations : []
744 | });
745 | } catch (error) {
746 | return formatResponse({
747 | success: false,
748 | error: `Error inspecting knowledge graph: ${error.message}`
749 | });
750 | }
751 | }
752 | else if (toolName === "create_entities") {
753 | const entities = params.entities;
754 | const zone = params.memory_zone;
755 |
756 | // First, check if any entities already exist or have empty names
757 | const conflictingEntities = [];
758 | const invalidEntities = [];
759 |
760 | for (const entity of entities) {
761 | // Check for empty names
762 | if (!entity.name || entity.name.trim() === '') {
763 | invalidEntities.push({
764 | name: "[empty]",
765 | reason: "Entity name cannot be empty"
766 | });
767 | continue;
768 | }
769 |
770 | const existingEntity = await kgClient.getEntity(entity.name, zone);
771 | if (existingEntity) {
772 | conflictingEntities.push(entity.name);
773 | }
774 | }
775 |
776 | // If there are conflicts or invalid entities, reject the operation
777 | if (conflictingEntities.length > 0 || invalidEntities.length > 0) {
778 | const zoneMsg = zone ? ` in zone "${zone}"` : "";
779 |
780 | // Fetch existing entity details if there are conflicts
781 | const existingEntitiesData = [];
782 | if (conflictingEntities.length > 0) {
783 | for (const entityName of conflictingEntities) {
784 | const existingEntity = await kgClient.getEntity(entityName, zone);
785 | if (existingEntity) {
786 | existingEntitiesData.push(existingEntity);
787 | }
788 | }
789 | }
790 |
791 | return formatResponse({
792 | success: false,
793 | error: `Entity creation failed${zoneMsg}, no entities were created.`,
794 | conflicts: conflictingEntities.length > 0 ? conflictingEntities : undefined,
795 | existingEntities: existingEntitiesData.length > 0 ? existingEntitiesData : undefined,
796 | invalidEntities: invalidEntities.length > 0 ? invalidEntities : undefined,
797 | message: conflictingEntities.length > 0 ?
798 | "Feel free to extend existing entities with more information if needed, or create entities with different names. Use update_entities to modify existing entities." :
799 | "Please provide valid entity names for all entities."
800 | });
801 | }
802 |
803 | // If no conflicts, proceed with entity creation
804 | const createdEntities = [];
805 | for (const entity of entities) {
806 | const savedEntity = await kgClient.saveEntity({
807 | name: entity.name,
808 | entityType: entity.entityType,
809 | observations: entity.observations,
810 | relevanceScore: entity.relevanceScore ?? 1.0
811 | }, zone);
812 |
813 | createdEntities.push(savedEntity);
814 | }
815 |
816 | return formatResponse({
817 | success: true,
818 | entities: createdEntities.map(e => ({
819 | name: e.name,
820 | entityType: e.entityType,
821 | observations: e.observations
822 | }))
823 | });
824 | }
825 | else if (toolName === "update_entities") {
826 | const entities = params.entities;
827 | const zone = params.memory_zone;
828 | const updatedEntities = [];
829 |
830 | for (const entity of entities) {
831 | // Get the existing entity first, then update with new values
832 | const existingEntity = await kgClient.getEntity(entity.name, zone);
833 | if (!existingEntity) {
834 | const zoneMsg = zone ? ` in zone "${zone}"` : "";
835 | throw new Error(`Entity "${entity.name}" not found${zoneMsg}`);
836 | }
837 |
838 | // Update with new values, preserving existing values for missing fields
839 | const updatedEntity = await kgClient.saveEntity({
840 | name: entity.name,
841 | entityType: entity.entityType || existingEntity.entityType,
842 | observations: entity.observations || existingEntity.observations,
843 | relevanceScore: entity.relevanceScore || ((existingEntity.relevanceScore ?? 1.0) * 2.0)
844 | }, zone);
845 |
846 | updatedEntities.push(updatedEntity);
847 | }
848 |
849 | return formatResponse({
850 | entities: updatedEntities.map(e => ({
851 | name: e.name,
852 | entityType: e.entityType,
853 | observations: e.observations
854 | }))
855 | });
856 | }
857 | else if (toolName === "delete_entities") {
858 | const names = params.names;
859 | const zone = params.memory_zone;
860 | const cascadeRelations = params.cascade_relations !== false; // Default to true
861 | const results = [];
862 | const invalidNames = [];
863 |
864 | // Validate names before attempting deletion
865 | for (const name of names) {
866 | if (!name || name.trim() === '') {
867 | invalidNames.push("[empty]");
868 | continue;
869 | }
870 | }
871 |
872 | // If there are invalid names, reject those operations
873 | if (invalidNames.length > 0) {
874 | return formatResponse({
875 | success: false,
876 | error: "Entity deletion failed for some entities",
877 | invalidNames,
878 | message: "Entity names cannot be empty"
879 | });
880 | }
881 |
882 | // Delete each valid entity individually
883 | for (const name of names) {
884 | try {
885 | const success = await kgClient.deleteEntity(name, zone, {
886 | cascadeRelations
887 | });
888 | results.push({ name, deleted: success });
889 | } catch (error) {
890 | results.push({ name, deleted: false, error: error.message });
891 | }
892 | }
893 |
894 | return formatResponse({
895 | success: true,
896 | results
897 | });
898 | }
899 | else if (toolName === "create_relations") {
900 | const relations = params.relations;
901 | const defaultZone = params.memory_zone;
902 | const autoCreateMissingEntities = params.auto_create_missing_entities !== false; // Default to true for backward compatibility
903 | const savedRelations = [];
904 | const failedRelations = [];
905 |
906 | for (const relation of relations) {
907 | const fromZone = relation.fromZone || defaultZone;
908 | const toZone = relation.toZone || defaultZone;
909 |
910 | try {
911 | const savedRelation = await kgClient.saveRelation({
912 | from: relation.from,
913 | to: relation.to,
914 | relationType: relation.type
915 | }, fromZone, toZone, { autoCreateMissingEntities });
916 |
917 | savedRelations.push(savedRelation);
918 | } catch (error) {
919 | failedRelations.push({
920 | relation,
921 | error: error.message
922 | });
923 | }
924 | }
925 |
926 | // If there were any failures, include them in the response
927 | if (failedRelations.length > 0) {
928 | return formatResponse({
929 | success: savedRelations.length > 0,
930 | relations: savedRelations.map(r => ({
931 | from: r.from,
932 | to: r.to,
933 | type: r.relationType,
934 | fromZone: r.fromZone,
935 | toZone: r.toZone
936 | })),
937 | failedRelations
938 | });
939 | }
940 |
941 | return formatResponse({
942 | success: true,
943 | relations: savedRelations.map(r => ({
944 | from: r.from,
945 | to: r.to,
946 | type: r.relationType,
947 | fromZone: r.fromZone,
948 | toZone: r.toZone
949 | }))
950 | });
951 | }
952 | else if (toolName === "delete_relations") {
953 | const relations = params.relations;
954 | const zone = params.memory_zone;
955 | const results = [];
956 |
957 | // Delete each relation individually
958 | for (const relation of relations) {
959 | const success = await kgClient.deleteRelation(
960 | relation.from,
961 | relation.to,
962 | relation.type,
963 | zone,
964 | zone
965 | );
966 | results.push({
967 | from: relation.from,
968 | to: relation.to,
969 | type: relation.type,
970 | deleted: success
971 | });
972 | }
973 |
974 | return formatResponse({
975 | success: true,
976 | results
977 | });
978 | }
979 | else if (toolName === "search_nodes") {
980 | const includeObservations = params.includeObservations ?? false;
981 | const zone = params.memory_zone;
982 |
983 | // Use the high-level userSearch method that handles AI filtering internally
984 | const { entities: filteredEntities, relations: formattedRelations } = await kgClient.userSearch({
985 | query: params.query,
986 | entityTypes: params.entityTypes,
987 | limit: params.limit,
988 | sortBy: params.sortBy,
989 | includeObservations,
990 | zone,
991 | informationNeeded: params.informationNeeded,
992 | reason: params.reason
993 | });
994 |
995 | return formatResponse({ entities: filteredEntities, relations: formattedRelations });
996 | }
997 | else if (toolName === "open_nodes") {
998 | const names = params.names || [];
999 | const zone = params.memory_zone;
1000 |
1001 | // Get the entities
1002 | const entities: ESEntity[] = [];
1003 | for (const name of names) {
1004 | const entity = await kgClient.getEntity(name, zone);
1005 | if (entity) {
1006 | entities.push(entity);
1007 | }
1008 | }
1009 |
1010 | // Format entities
1011 | const formattedEntities = entities.map(e => ({
1012 | name: e.name,
1013 | entityType: e.entityType,
1014 | observations: e.observations
1015 | }));
1016 |
1017 | // Get relations between these entities
1018 | const entityNames = formattedEntities.map(e => e.name);
1019 | const { relations } = await kgClient.getRelationsForEntities(entityNames, zone);
1020 |
1021 | // Map relations to the expected format
1022 | const formattedRelations = relations.map(r => ({
1023 | from: r.from,
1024 | to: r.to,
1025 | type: r.relationType,
1026 | fromZone: r.fromZone,
1027 | toZone: r.toZone
1028 | }));
1029 |
1030 | return formatResponse({ entities: formattedEntities, relations: formattedRelations });
1031 | }
1032 | else if (toolName === "add_observations") {
1033 | const name = params.name;
1034 | const observations = params.observations;
1035 | const zone = params.memory_zone;
1036 |
1037 | // Get existing entity
1038 | const entity = await kgClient.getEntity(name, zone);
1039 | if (!entity) {
1040 | const zoneMsg = zone ? ` in zone "${zone}"` : "";
1041 | return formatResponse({
1042 | success: false,
1043 | error: `Entity "${name}" not found${zoneMsg}`,
1044 | message: "Please create the entity before adding observations."
1045 | });
1046 | }
1047 |
1048 | // Add observations to the entity
1049 | const updatedEntity = await kgClient.addObservations(name, observations, zone);
1050 |
1051 | return formatResponse({
1052 | success: true,
1053 | entity: updatedEntity
1054 | });
1055 | }
1056 | else if (toolName === "mark_important") {
1057 | const name = params.name;
1058 | const important = params.important;
1059 | const zone = params.memory_zone;
1060 | const autoCreate = params.auto_create === true;
1061 |
1062 | try {
1063 | // Mark the entity as important, with auto-creation if specified
1064 | const updatedEntity = await kgClient.markImportant(name, important, zone, {
1065 | autoCreateMissingEntities: autoCreate
1066 | });
1067 |
1068 | return formatResponse({
1069 | success: true,
1070 | entity: updatedEntity,
1071 | auto_created: autoCreate && !(await kgClient.getEntity(name, zone))
1072 | });
1073 | } catch (error) {
1074 | const zoneMsg = zone ? ` in zone "${zone}"` : "";
1075 | return formatResponse({
1076 | success: false,
1077 | error: `Entity "${name}" not found${zoneMsg}`,
1078 | message: "Please create the entity before marking it as important, or set auto_create to true."
1079 | });
1080 | }
1081 | }
1082 | else if (toolName === "get_recent") {
1083 | const limit = params.limit || 20;
1084 | const includeObservations = params.includeObservations ?? false;
1085 | const zone = params.memory_zone;
1086 |
1087 | const recentEntities = await kgClient.getRecentEntities(limit, includeObservations, zone);
1088 |
1089 | return formatResponse({
1090 | entities: recentEntities.map(e => ({
1091 | name: e.name,
1092 | entityType: e.entityType,
1093 | observations: e.observations
1094 | })),
1095 | total: recentEntities.length
1096 | });
1097 | }
1098 | else if (toolName === "list_zones") {
1099 | const reason = params.reason;
1100 | const zones = await kgClient.listMemoryZones(reason);
1101 |
1102 | // If reason is provided and GroqAI is available, use AI to score zone usefulness
1103 | if (reason && GroqAI.isEnabled && zones.length > 0) {
1104 | try {
1105 | // Get usefulness scores for each zone
1106 | const usefulnessScores = await GroqAI.classifyZoneUsefulness(zones, reason);
1107 |
1108 | // Process zones based on their usefulness scores
1109 | const processedZones = zones.map(zone => {
1110 | const usefulness = usefulnessScores[zone.name] !== undefined ?
1111 | usefulnessScores[zone.name] : 2; // Default to very useful (2) if not classified
1112 |
1113 | // Format zone info based on usefulness score
1114 | if (usefulness === 0) { // Not useful
1115 | return {
1116 | name: zone.name,
1117 | usefulness: 'not useful'
1118 | };
1119 | } else if (usefulness === 1) { // A little useful
1120 | return {
1121 | name: zone.name,
1122 | description: zone.description,
1123 | usefulness: 'a little useful'
1124 | };
1125 | } else { // Very useful (2) or default
1126 | return {
1127 | name: zone.name,
1128 | description: zone.description,
1129 | created_at: zone.createdAt,
1130 | last_modified: zone.lastModified,
1131 | config: zone.config,
1132 | usefulness: 'very useful'
1133 | };
1134 | }
1135 | });
1136 |
1137 | // Sort zones by usefulness (most useful first)
1138 | processedZones.sort((a, b) => {
1139 | const scoreA = usefulnessScores[a.name] !== undefined ? usefulnessScores[a.name] : 2;
1140 | const scoreB = usefulnessScores[b.name] !== undefined ? usefulnessScores[b.name] : 2;
1141 | return scoreB - scoreA; // Descending order
1142 | });
1143 |
1144 | return formatResponse({
1145 | zones: processedZones
1146 | });
1147 | } catch (error) {
1148 | console.error('Error classifying zones:', error);
1149 | // Fall back to default behavior
1150 | }
1151 | }
1152 |
1153 | // Default behavior (no reason provided or AI failed)
1154 | return formatResponse({
1155 | zones: zones.map(zone => ({
1156 | name: zone.name,
1157 | description: zone.description,
1158 | created_at: zone.createdAt,
1159 | last_modified: zone.lastModified,
1160 | usefulness: 'very useful' // Default to very useful when no AI filtering is done
1161 | }))
1162 | });
1163 | }
1164 | else if (toolName === "create_zone") {
1165 | const name = params.name;
1166 | const description = params.description;
1167 |
1168 | try {
1169 | await kgClient.addMemoryZone(name, description);
1170 |
1171 | return formatResponse({
1172 | success: true,
1173 | zone: name,
1174 | message: `Zone "${name}" created successfully`
1175 | });
1176 | } catch (error) {
1177 | return formatResponse({
1178 | success: false,
1179 | error: `Failed to create zone: ${(error as Error).message}`
1180 | });
1181 | }
1182 | }
1183 | else if (toolName === "delete_zone") {
1184 | const name = params.name;
1185 | const confirm = params.confirm === true;
1186 |
1187 | if (!confirm) {
1188 | return formatResponse({
1189 | success: false,
1190 | error: "Confirmation required. Set confirm=true to proceed with deletion."
1191 | });
1192 | }
1193 |
1194 | try {
1195 | const result = await kgClient.deleteMemoryZone(name);
1196 |
1197 | if (result) {
1198 | return formatResponse({
1199 | success: true,
1200 | message: `Zone "${name}" deleted successfully`
1201 | });
1202 | } else {
1203 | return formatResponse({
1204 | success: false,
1205 | error: `Failed to delete zone "${name}"`
1206 | });
1207 | }
1208 | } catch (error) {
1209 | return formatResponse({
1210 | success: false,
1211 | error: `Error deleting zone: ${(error as Error).message}`
1212 | });
1213 | }
1214 | }
1215 | else if (toolName === "copy_entities") {
1216 | const names = params.names;
1217 | const sourceZone = params.source_zone;
1218 | const targetZone = params.target_zone;
1219 | const copyRelations = params.copy_relations !== false;
1220 | const overwrite = params.overwrite === true;
1221 |
1222 | try {
1223 | const result = await kgClient.copyEntitiesBetweenZones(
1224 | names,
1225 | sourceZone,
1226 | targetZone,
1227 | {
1228 | copyRelations,
1229 | overwrite
1230 | }
1231 | );
1232 |
1233 | return formatResponse({
1234 | success: result.entitiesCopied.length > 0,
1235 | entities_copied: result.entitiesCopied,
1236 | entities_skipped: result.entitiesSkipped,
1237 | relations_copied: result.relationsCopied
1238 | });
1239 | } catch (error) {
1240 | return formatResponse({
1241 | success: false,
1242 | error: `Error copying entities: ${(error as Error).message}`
1243 | });
1244 | }
1245 | }
1246 | else if (toolName === "move_entities") {
1247 | const names = params.names;
1248 | const sourceZone = params.source_zone;
1249 | const targetZone = params.target_zone;
1250 | const moveRelations = params.move_relations !== false;
1251 | const overwrite = params.overwrite === true;
1252 |
1253 | try {
1254 | const result = await kgClient.moveEntitiesBetweenZones(
1255 | names,
1256 | sourceZone,
1257 | targetZone,
1258 | {
1259 | moveRelations,
1260 | overwrite
1261 | }
1262 | );
1263 |
1264 | return formatResponse({
1265 | success: result.entitiesMoved.length > 0,
1266 | entities_moved: result.entitiesMoved,
1267 | entities_skipped: result.entitiesSkipped,
1268 | relations_moved: result.relationsMoved
1269 | });
1270 | } catch (error) {
1271 | return formatResponse({
1272 | success: false,
1273 | error: `Error moving entities: ${(error as Error).message}`
1274 | });
1275 | }
1276 | }
1277 | else if (toolName === "merge_zones") {
1278 | const sourceZones = params.source_zones;
1279 | const targetZone = params.target_zone;
1280 | const deleteSourceZones = params.delete_source_zones === true;
1281 | const overwriteConflicts = params.overwrite_conflicts || 'skip';
1282 |
1283 | try {
1284 | const result = await kgClient.mergeZones(
1285 | sourceZones,
1286 | targetZone,
1287 | {
1288 | deleteSourceZones,
1289 | overwriteConflicts: overwriteConflicts as 'skip' | 'overwrite' | 'rename'
1290 | }
1291 | );
1292 |
1293 | return formatResponse({
1294 | success: result.mergedZones.length > 0,
1295 | merged_zones: result.mergedZones,
1296 | failed_zones: result.failedZones,
1297 | entities_copied: result.entitiesCopied,
1298 | entities_skipped: result.entitiesSkipped,
1299 | relations_copied: result.relationsCopied
1300 | });
1301 | } catch (error) {
1302 | return formatResponse({
1303 | success: false,
1304 | error: `Error merging zones: ${(error as Error).message}`
1305 | });
1306 | }
1307 | }
1308 | else if (toolName === "zone_stats") {
1309 | const zone = params.zone;
1310 |
1311 | try {
1312 | const stats = await kgClient.getMemoryZoneStats(zone);
1313 |
1314 | return formatResponse({
1315 | zone: stats.zone,
1316 | entity_count: stats.entityCount,
1317 | relation_count: stats.relationCount,
1318 | entity_types: stats.entityTypes,
1319 | relation_types: stats.relationTypes
1320 | });
1321 | } catch (error) {
1322 | return formatResponse({
1323 | success: false,
1324 | error: `Error getting zone stats: ${(error as Error).message}`
1325 | });
1326 | }
1327 | }
1328 | else if (toolName === "get_time_utc") {
1329 | // Get current time in UTC
1330 | const now = new Date();
1331 |
1332 | // Format time as YYYY-MM-DD hh:mm:ss
1333 | const pad = (num: number) => num.toString().padStart(2, '0');
1334 |
1335 | const year = now.getUTCFullYear();
1336 | const month = pad(now.getUTCMonth() + 1); // months are 0-indexed
1337 | const day = pad(now.getUTCDate());
1338 | const hours = pad(now.getUTCHours());
1339 | const minutes = pad(now.getUTCMinutes());
1340 | const seconds = pad(now.getUTCSeconds());
1341 |
1342 | const formattedTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
1343 |
1344 | return formatResponse({
1345 | utc_time: formattedTime
1346 | });
1347 | }
1348 | });
1349 |
1350 | return server;
1351 | }
1352 |
1353 | // Start the server with proper transport and error handling
1354 | async function initServer() {
1355 | const server = await startServer();
1356 |
1357 | // Connect the server to the transport
1358 | const transport = new StdioServerTransport();
1359 | await server.connect(transport);
1360 | console.error('MCP server running on stdio');
1361 | }
1362 |
1363 | // Initialize with error handling
1364 | initServer().catch(error => {
1365 | console.error('Error starting server:', error);
1366 | process.exit(1);
1367 | });
```