├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── MemoryViewer.html ├── README.md ├── SchemaManager.html ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── application │ ├── index.ts │ ├── managers │ │ ├── ApplicationManager.ts │ │ ├── EdgeManager.ts │ │ ├── GraphManager.ts │ │ ├── ManagerFactory.ts │ │ ├── MetadataManager.ts │ │ ├── NodeManager.ts │ │ ├── SearchManager.ts │ │ ├── TransactionManager.ts │ │ ├── base │ │ │ └── BaseManager.ts │ │ ├── index.ts │ │ └── interfaces │ │ │ ├── IEdgeManager.ts │ │ │ ├── IManager.ts │ │ │ ├── IManagerOperations.ts │ │ │ ├── IMetadataManager.ts │ │ │ ├── INodeManager.ts │ │ │ ├── ISearchManager.ts │ │ │ ├── ITransactionManager.ts │ │ │ └── index.ts │ └── operations │ │ ├── GraphOperations.ts │ │ ├── SearchOperations.ts │ │ ├── TransactionOperations.ts │ │ └── index.ts ├── config │ ├── config.ts │ └── index.ts ├── core │ ├── graph │ │ ├── Edge.ts │ │ ├── EdgeWeightUtils.ts │ │ ├── Graph.ts │ │ ├── GraphValidator.ts │ │ ├── Node.ts │ │ └── index.ts │ ├── index.ts │ ├── metadata │ │ ├── Metadata.ts │ │ ├── MetadataProcessor.ts │ │ └── index.ts │ └── schema │ │ ├── SchemaBuilder.ts │ │ ├── SchemaLoader.ts │ │ ├── SchemaProcessor.ts │ │ └── index.ts ├── data │ ├── memory.json │ └── schemas │ │ ├── artifact.schema.json │ │ ├── currency.schema.json │ │ ├── faction.schema.json │ │ ├── inventory.schema.json │ │ ├── location.schema.json │ │ ├── npc.schema.json │ │ ├── player_character.schema.json │ │ ├── quest.schema.json │ │ ├── skills.schema.json │ │ ├── temporal.schema.json │ │ └── transportation.schema.json ├── index.ts ├── infrastructure │ ├── events │ │ ├── EventEmitter.ts │ │ ├── EventTypes.ts │ │ └── index.ts │ ├── index.ts │ └── storage │ │ ├── IStorage.ts │ │ ├── JsonLineStorage.ts │ │ └── index.ts ├── integration │ ├── index.ts │ └── tools │ │ ├── DynamicSchemaToolRegistry.ts │ │ ├── callToolHandler.ts │ │ ├── handlers │ │ ├── BaseToolHandler.ts │ │ ├── DynamicToolHandler.ts │ │ ├── GraphToolHandler.ts │ │ ├── MetadataToolHandler.ts │ │ ├── SearchToolHandler.ts │ │ ├── ToolHandlerFactory.ts │ │ └── index.ts │ │ ├── index.ts │ │ └── registry │ │ ├── dynamicTools.ts │ │ ├── index.ts │ │ ├── staticTools.ts │ │ └── toolsRegistry.ts └── shared │ ├── index.ts │ ├── types │ ├── index.ts │ ├── operations.ts │ └── tools.ts │ └── utils │ ├── index.ts │ └── responseFormatter.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-error.log* 5 | 6 | # TypeScript 7 | dist/ 8 | 9 | # System Files (macOS) 10 | .DS_Store 11 | 12 | # IDE-specific files 13 | .idea/ 14 | 15 | # Log files 16 | *.log 17 | *.log.* 18 | 19 | # Temporary files 20 | *.tmp 21 | *.temp 22 | 23 | # OS generated files 24 | Thumbs.db 25 | ehthumbs.db 26 | 27 | # dotenv 28 | .env 29 | 30 | # Ignore cache and log files created by npm 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | .pnpm-debug.log* 35 | 36 | # Coverage reports 37 | coverage/ 38 | .nyc_output/ 39 | 40 | # Ignore the memory file 41 | src/data/memory.json 42 | 43 | # Ignore schema changes 44 | scr/data/schemas/*.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### v0.2.8 (2024-12-24) 4 | 5 | #### Features 6 | 7 | - **Edge Weights:** 8 | - Introduced an optional `weight` property to the `Edge` interface to represent relationship strength in the range of 0-1. 9 | - Edge weights now default to 1 if not specified. 10 | 11 | - **Enhanced Search:** 12 | - Modified `SearchManager` to include immediate neighbor nodes in `searchNodes` and `openNodes` results. 13 | 14 | **Impact:** 15 | 16 | - **Edge Weights:** 17 | - Enables a more nuanced representation of relationships in the knowledge graph, allowing for the expression of varying degrees of connection strength or confidence. 18 | - No changes in schemas. 19 | - 20 | - **Enhanced Search:** 21 | - Provides a more comprehensive view of the relevant portion of the knowledge graph returning more contextually relevant information to the AI. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use a Node.js image with Node.js 18 3 | FROM node:18-alpine AS builder 4 | 5 | # Set working directory 6 | WORKDIR /app 7 | 8 | # Copy package files and install dependencies 9 | COPY package.json package-lock.json ./ 10 | RUN npm install --ignore-scripts 11 | 12 | # Copy source files 13 | COPY . . 14 | 15 | # Build the TypeScript project 16 | RUN npm run build 17 | 18 | # Use a minimal Node.js runtime for the final image 19 | FROM node:18-alpine 20 | 21 | # Set working directory 22 | WORKDIR /app 23 | 24 | # Copy the built files from the builder stage 25 | COPY --from=builder /app/dist /app/dist 26 | COPY --from=builder /app/package.json /app/package-lock.json ./ 27 | 28 | # Install only production dependencies 29 | RUN npm ci --omit=dev 30 | 31 | # Expose necessary ports (if applicable) 32 | # EXPOSE 3000 # Example port, adjust as necessary 33 | 34 | # Start the server 35 | CMD ["node", "dist/index.js"] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CheMiguel23 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MemoryMesh 2 | [![Release](https://img.shields.io/badge/Release-v0.2.8-blue.svg)](./CHANGELOG.md) 3 | [![smithery badge](https://smithery.ai/badge/memorymesh)](https://smithery.ai/server/memorymesh) 4 | ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC.svg?logo=typescript&logoColor=white) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | ![GitHub Stars](https://img.shields.io/github/stars/CheMiguel23/MemoryMesh.svg?style=social) 7 | 8 | MemoryMesh is a knowledge graph server designed for AI models, with a focus on text-based RPGs and interactive storytelling. It helps AI maintain consistent, structured memory across conversations, enabling richer and more dynamic interactions. 9 | 10 | *The project is based on the [Knowledge Graph Memory Server](https://github.com/modelcontextprotocol/servers/tree/main/src/memory) from the MCP servers repository and retains its core functionality.* 11 | 12 | MemoryMesh MCP server 13 | 14 | ## IMPORTANT 15 | Since `v0.2.7` the default location of schemas was changed to `dist/data/schemas`. 16 | This location is not expected to change in the future, but if you are updating from a previous version, make sure to move your schema files to the new location. 17 | 18 | ## Quick Links 19 | 20 | * [Installation](#installation) 21 | * [Example](#example) 22 | * [SchemaManager Guide Discussion](https://github.com/CheMiguel23/MemoryMesh/discussions/3) 23 | * [MemoryViewer Guide Discussion](https://github.com/CheMiguel23/MemoryMesh/discussions/15) 24 | 25 | ## Overview 26 | 27 | MemoryMesh is a local knowledge graph server that empowers you to build and manage structured information for AI models. While particularly well-suited for text-based RPGs, its adaptable design makes it useful for various applications, including social network simulations, organizational planning, or any scenario involving structured data. 28 | 29 | ### Key Features 30 | 31 | * **Dynamic Schema-Based Tools:** Define your data structure with schemas, and MemoryMesh automatically generates tools for adding, updating, and deleting data. 32 | * **Intuitive Schema Design:** Create schemas that guide the AI in generating and connecting nodes, using required fields, enumerated types, and relationship definitions. 33 | * **Metadata for AI Guidance:** Use metadata to provide context and structure, helping the AI understand the meaning and relationships within your data. 34 | * **Relationship Handling:** Define relationships within your schemas to encourage the AI to create connections (edges) between related data points (nodes). 35 | * **Informative Feedback:** Provides error feedback to the AI, enabling it to learn from mistakes and improve its interactions with the knowledge graph. 36 | * **Event Support:** An event system tracks operations, providing insights into how the knowledge graph is being modified. 37 | 38 | #### Nodes 39 | 40 | Nodes represent entities or concepts within the knowledge graph. Each node has: 41 | 42 | * `name`: A unique identifier. 43 | * `nodeType`: The type of the node (e.g., `npc`, `artifact`, `location`), defined by your schemas. 44 | * `metadata`: An array of strings providing descriptive details about the node. 45 | * `weight`: (Optional) A numerical value between 0 and 1 representing the strength of the relationship, defaulting to 1. 46 | 47 | **Example Node:** 48 | 49 | ```json 50 | { 51 | "name": "Aragorn", 52 | "nodeType": "player_character", 53 | "metadata": [ 54 | "Race: Human", 55 | "Class: Ranger", 56 | "Skills: Tracking, Swordsmanship", 57 | "Affiliation: Fellowship of the Ring" 58 | ] 59 | } 60 | ``` 61 | 62 | #### Edges 63 | 64 | Edges represent relationships between nodes. Each edge has: 65 | 66 | * `from`: The name of the source node. 67 | * `to`: The name of the target node. 68 | * `edgeType`: The type of relationship (e.g., `owns`, `located_in`). 69 | 70 | ```json 71 | { 72 | "from": "Aragorn", 73 | "to": "Andúril", 74 | "edgeType": "owns" 75 | } 76 | ``` 77 | 78 | #### Schemas 79 | 80 | Schemas are the heart of MemoryMesh. They define the structure of your data and drive the automatic generation of tools. 81 | 82 | ##### Schema File Location 83 | 84 | Place your schema files (`.schema.json`) in the `dist/data/schemas` directory of your built MemoryMesh project. MemoryMesh will automatically detect and process these files on startup. 85 | 86 | ##### Schema Structure 87 | 88 | File name: `[name].schema.json`. For example, for a schema defining an 'npc', the filename would be `add_npc.schema.json`. 89 | 90 | * `name` - Identifier for the schema and node type within the memory. **IMPORTANT**: The schema’s name *must* start with `add_` to be recognized. 91 | * `description` - Used as the description for the `add_` tool, providing context for the AI. *(The `delete` and `update` tools have a generic description)* 92 | * `properties` - Each property includes its type, description, and additional constraints. 93 | * `property` 94 | * `type` - Supported values are `string` or `array`. 95 | * `description` - Helps guide the AI on the entity’s purpose. 96 | * `required` - Boolean. If `true`, the **AI is forced** to provide this property when creating a node. 97 | * `enum` - An array of strings. If present, the **AI must choose** one of the given options. 98 | * `relationship` - Defines a connection to another node. If a property is required and has a relationship, the **AI will always create** both the node and the corresponding edge. 99 | * `edgeType` - Type of the relationship to be created. 100 | * `description` - Helps guide the AI on the relationship’s purpose. 101 | * `additionalProperties` - Boolean. If `true`, allows the AI to add extra attributes beyond those defined as required or optional. 102 | 103 | ##### Example Schema (add_npc.schema.json): 104 | 105 | ```json 106 | { 107 | "name": "add_npc", 108 | "description": "Schema for adding an NPC to the memory" , 109 | "properties": { 110 | "name": { 111 | "type": "string", 112 | "description": "A unique identifier for the NPC", 113 | "required": true 114 | }, 115 | "race": { 116 | "type": "string", 117 | "description": "The species or race of the NPC", 118 | "required": true, 119 | "enum": [ 120 | "Human", 121 | "Elf", 122 | "Dwarf", 123 | "Orc", 124 | "Goblin" 125 | ] 126 | }, 127 | "currentLocation": { 128 | "type": "string", 129 | "description": "The current location of the NPC", 130 | "required": true, 131 | "relationship": { 132 | "edgeType": "located_in", 133 | "description": "The current location of the NPC" 134 | } 135 | } 136 | }, 137 | "additionalProperties": true 138 | } 139 | ``` 140 | 141 | Based on this schema, MemoryMesh automatically creates: 142 | * add_npc: To add new NPC nodes. 143 | * update_npc: To modify existing NPC nodes. 144 | * delete_npc: To remove NPC nodes. 145 | 146 | MemoryMesh includes 11 pre-built schemas designed for text-based RPGs, providing a ready-to-use foundation for game development. 147 | 148 | ##### SchemaManager Tool 149 | 150 | MemoryMesh includes a [SchemaManager tool](https://github.com/CheMiguel23/MemoryMesh/blob/main/SchemaManager.html) to simplify schema creation and editing. It provides a visual interface, making it easy to define your data structures without writing JSON directly. 151 | 152 | image 153 | 154 | ### Dynamic Tools 155 | 156 | MemoryMesh simplifies interaction with your knowledge graph through **dynamic tools**. These tools are not manually coded but are **automatically generated** directly from your **schema definitions**. This means that when you define the structure of your data using schemas, MemoryMesh intelligently creates a set of tools tailored to work with that specific data structure. 157 | 158 | **Think of it like this:** You provide a blueprint (the schema), and MemoryMesh automatically constructs the necessary tools to build, modify, and remove elements based on that blueprint. 159 | 160 | #### How does it work behind the scenes? 161 | 162 | MemoryMesh has an intelligent system that reads your schema definitions. It analyzes the structure you've defined, including the properties of your entities and their relationships. Based on this analysis, it automatically creates a set of tools for each entity type: 163 | 164 | * **`add_`:** A tool for creating new instances of an entity. 165 | * **`update_`:** A tool for modifying existing entities. 166 | * **`delete_`:** A tool for removing entities. 167 | 168 | These tools are then made available through a central hub within MemoryMesh, ensuring they can be easily accessed and used by any connected client or AI. 169 | 170 | **In essence, MemoryMesh's dynamic tool system provides a powerful and efficient way to manage your knowledge graph, freeing you to focus on the content and logic of your application rather than the underlying mechanics of data manipulation.** 171 | 172 | ### Memory file 173 | 174 | By default, data is stored in a JSON file in `dist/data/memory.json`. 175 | 176 | #### Memory Viewer 177 | 178 | The Memory Viewer is a separate tool designed to help you visualize and inspect the contents of the knowledge graph managed by MemoryMesh. It provides a user-friendly interface for exploring nodes, edges, and their properties. 179 | 180 | ##### Key Features: 181 | * Graph Visualization: View the knowledge graph as an interactive node-link diagram. 182 | * Node Inspection: Select nodes to see their nodeType, metadata, and connected edges. 183 | * Edge Exploration: Examine relationships between nodes, including edgeType and direction. 184 | * Search and Filtering: Quickly find specific nodes or filter them by type. 185 | * Table View: Allows you to easily find and inspect specific nodes and edges, or all of them at once. 186 | * Raw JSON View: Allows you to view the raw JSON data from the memory file. 187 | * Stats Panel: Provides key metrics and information about the knowledge graph: total nodes, total edges, node types, and edge types. 188 | * Search and Filter: Allows you to filter by node type or edge type and filter whether to show nodes, edges, or both. 189 | 190 | ##### Accessing the Memory Viewer 191 | The Memory Viewer is a standalone web application. [Memory Viewer discussion](https://github.com/CheMiguel23/MemoryMesh/discussions/15) 192 | 193 | ##### Using the Memory Viewer 194 | * Select Memory File: In the Memory Viewer, click the "Select Memory File" button. 195 | * Choose File: Navigate to your MemoryMesh project directory and select the `memory.json` file (located in `dist/data/memory.json` by default). 196 | * Explore: The Memory Viewer will load and display the contents of your knowledge graph. 197 | 198 | ## Memory Flow 199 | 200 | ![image](https://github.com/user-attachments/assets/27519003-c1e6-448a-9fdb-cd0a0009f67d) 201 | 202 | ## Prompt 203 | 204 | For optimal results, use Claude's "Projects" feature with custom instructions. Here's an example of a prompt you can start with: 205 | 206 | ``` 207 | You are a helpful AI assistant managing a knowledge graph for a text-based RPG. You have access to the following tools: add_npc, update_npc, delete_npc, add_location, update_location, delete_location, and other tools for managing the game world. 208 | 209 | When the user provides input, first process it using your available tools to update the knowledge graph. Then, respond in a way that is appropriate for a text-based RPG. 210 | ``` 211 | 212 | You can also instruct the AI to perform specific actions directly in the chat. 213 | 214 | Experiment with different prompts to find what works best for your use case! 215 | 216 | ### Example 217 | 1. A [simple example](https://pastebin.com/0HvKg5FZ) with custom instructions. 218 | 2. An example for the sake of example, with visualization _(NOT part of the functionality)_ 219 | 220 | > Add a couple of cities, some npcs, couple locations around the city to explore, hide an artifact or two somewhere 221 | 222 | ![image](https://github.com/user-attachments/assets/508d5903-2896-4665-a892-cdb7b81dfba6) 223 | 224 | ## Installation 225 | 226 | ### Installing via Smithery 227 | 228 | To install MemoryMesh for Claude Desktop automatically via [Smithery](https://smithery.ai/server/memorymesh): 229 | 230 | ```bash 231 | npx -y @smithery/cli install memorymesh --client claude 232 | ``` 233 | 234 | ### Prerequisites 235 | 236 | * **Node.js:** Version 18 or higher. You can download it from [nodejs.org](https://nodejs.org/). 237 | * **npm:** Usually included with Node.js. 238 | * **Claude for Desktop:** Make sure you have the latest version installed from [claude.ai/download](https://claude.ai/download). 239 | 240 | ### Installation Steps 241 | 242 | 1. **Clone the Repository:** 243 | 244 | ```bash 245 | git clone https://github.com/CheMiguel23/memorymesh.git 246 | cd memorymesh 247 | ``` 248 | 249 | 2. **Install Dependencies:** 250 | 251 | ```bash 252 | npm install 253 | ``` 254 | 255 | 3. **Build the Project:** 256 | 257 | ```bash 258 | npm run build 259 | ``` 260 | This command compiles the TypeScript code into JavaScript in the `dist` directory and copies sample schema and data files into it as well. 261 | 262 | 4. **Verify File Copy (Optional):** 263 | 264 | * The build process should automatically copy the `data` folder to `dist`. 265 | * **Check** that `dist/data` exists and contains `.json` files. Also verify that `dist/data/schemas` exists and contains `.schema.json` files. 266 | 267 | 5. **Configure Claude Desktop:** 268 | 269 | Open your Claude Desktop configuration file: 270 | 271 | * **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` 272 | * **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` 273 | * Add an entry for `memorymesh` to the `mcpServers` section. You can choose **one** of the following configuration options: 274 | 275 | ```json 276 | "mcpServers": { 277 | "memorymesh": { 278 | "command": "node", 279 | "args": ["/ABSOLUTE/PATH/TO/YOUR/PROJECT/memorymesh/dist/index.js"] 280 | } 281 | } 282 | ``` 283 | 284 | * Replace `/ABSOLUTE/PATH/TO/YOUR/PROJECT/` with the **actual absolute path** to your `memorymesh` project directory. 285 | * **Example (macOS):** 286 | ```json 287 | "command": "node", 288 | "args": ["/Users/yourusername/Projects/memorymesh/dist/index.js"] 289 | ``` 290 | * **Example (Windows):** 291 | ```json 292 | "command": "node", 293 | "args": ["C:\\Projects\\memorymesh\\dist\\index.js"] 294 | ``` 295 | 296 | 6. **Restart Claude Desktop:** Completely restart Claude Desktop for the changes to take effect. 297 | 298 | ### Verify Installation 299 | 300 | 1. Start Claude Desktop. 301 | 2. Open a new chat. 302 | 3. Look for the MCP plugin icon in the top-right corner. If it's there, your configuration is likely correct. 303 | 4. Click the icon. You should see "memorymesh" in the list of connected servers. 304 | 5. Click the icon. If you see tools listed (e.g., `add_npc`, `update_npc`, etc.), your server is working and exposing tools correctly. 305 | 306 | ### Updating 307 | Before updates, make sure to back up your `dist/data` directory to avoid losing your memory data. 308 | 309 | ### Troubleshooting 310 | 311 | * **Server not appearing in Claude:** 312 | * Double-check the paths in your `claude_desktop_config.json`. Make sure they are absolute paths and correct. 313 | * Verify that the `dist` directory exists and contains the compiled JavaScript files, including `index.js`. 314 | * Check the Claude Desktop logs for errors: 315 | * **macOS:** `~/Library/Logs/Claude/mcp-server-memorymesh.log` (and `mcp.log`) 316 | * **Windows:** (Likely in a `Logs` folder under `%AppData%\Claude`) 317 | 318 | * **Tools not showing up:** 319 | * Make sure your `npm run build` command completed without errors. 320 | * Verify that your schema files are correctly placed in `dist/data/schemas` and follow the correct naming convention (`add_[entity].schema.json`). 321 | * Check your server's console output or logs for any errors during initialization. 322 | 323 | ## Advanced Configuration 324 | MemoryMesh offers several ways to customize its behavior beyond the basic setup: 325 | 326 | ### Variables 327 | You can override default settings using in `/config/config.ts` 328 | * MEMORY_FILE: Specifies the path to the JSON file used for storing the knowledge graph data. (Default: `dist/data/memory.json`) 329 | * SCHEMAS_DIR: Path to schema files directory. (Default: `dist/data/schemas/memory.json`) 330 | 331 | ## Limitations 332 | 333 | 1. **Node Deletion:** The AI may be hesitant to delete nodes from the knowledge graph. Encourage it through prompts if needed. 334 | 335 | ## Contribution 336 | 337 | Contributions, feedback, and ideas are welcome! 338 | This project is a personal exploration into integrating structured data with AI reasoning capabilities. Contributions, feedback, and ideas are welcome to push it further or inspire new projects. 339 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memorymesh", 3 | "version": "0.2.8", 4 | "description": "An MCP server that uses a knowledge graph to store and recall structured memory for AI models", 5 | "license": "MIT", 6 | "author": "CheMiguel23", 7 | "homepage": "https://github.com/CheMiguel23/memorymesh", 8 | "bugs": "https://github.com/CheMiguel23/memorymesh/issues", 9 | "type": "module", 10 | "main": "dist/index.js", 11 | "bin": { 12 | "memorymesh": "./dist/index.js" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "tsc && tsc-alias && shx chmod +x dist/*.js && copyfiles -u 1 src/data/schemas/*.json src/data/*.json dist/", 19 | "prepare": "npm run build", 20 | "watch": "tsc --watch", 21 | "start": "ts-node --esm src/index.ts", 22 | "start:claude": "ts-node --esm src/index.ts", 23 | "start:prod": "node dist/index.js" 24 | }, 25 | "dependencies": { 26 | "@modelcontextprotocol/sdk": "^0.5.0" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^20.11.24", 30 | "copyfiles": "^2.4.1", 31 | "shx": "^0.3.4", 32 | "ts-node": "^10.9.2", 33 | "tsc-alias": "^1.8.8", 34 | "typescript": "^5.3.3" 35 | } 36 | } -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: [] 9 | properties: {} 10 | commandFunction: 11 | # A function that produces the CLI command to start the MCP on stdio. 12 | |- 13 | (config) => ({ command: 'node', args: ['dist/index.js'] }) -------------------------------------------------------------------------------- /src/application/index.ts: -------------------------------------------------------------------------------- 1 | // src/application/index.ts 2 | 3 | export {ApplicationManager} from './managers/ApplicationManager.js'; 4 | export * from './managers/index.js'; 5 | export * from './managers/interfaces/index.js'; 6 | export * from './operations/index.js'; -------------------------------------------------------------------------------- /src/application/managers/ApplicationManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/ApplicationManager.ts 2 | 3 | import type {IStorage} from '@infrastructure/index.js'; 4 | import type { 5 | Node, 6 | Edge, 7 | Graph, 8 | MetadataAddition, 9 | MetadataDeletion, 10 | MetadataResult, 11 | } from '@core/index.js'; 12 | import type { 13 | EdgeFilter, 14 | GetEdgesResult, 15 | OpenNodesResult 16 | } from '@shared/index.js'; 17 | import { 18 | GraphManager, 19 | SearchManager, 20 | TransactionManager 21 | } from "@application/index.js"; 22 | import {JsonLineStorage} from '@infrastructure/index.js'; 23 | 24 | /** 25 | * Main facade that coordinates between specialized managers 26 | */ 27 | export class ApplicationManager { 28 | private readonly graphManager: GraphManager; 29 | private readonly searchManager: SearchManager; 30 | private readonly transactionManager: TransactionManager; 31 | 32 | constructor(storage: IStorage = new JsonLineStorage()) { 33 | this.graphManager = new GraphManager(storage); 34 | this.searchManager = new SearchManager(storage); 35 | this.transactionManager = new TransactionManager(storage); 36 | } 37 | 38 | // Graph operations delegated to GraphManager 39 | async addNodes(nodes: Node[]): Promise { 40 | return this.graphManager.addNodes(nodes); 41 | } 42 | 43 | async updateNodes(nodes: Partial[]): Promise { 44 | return this.graphManager.updateNodes(nodes); 45 | } 46 | 47 | async deleteNodes(nodeNames: string[]): Promise { 48 | return this.graphManager.deleteNodes(nodeNames); 49 | } 50 | 51 | async addEdges(edges: Edge[]): Promise { 52 | return this.graphManager.addEdges(edges); 53 | } 54 | 55 | async updateEdges(edges: Edge[]): Promise { 56 | return this.graphManager.updateEdges(edges); 57 | } 58 | 59 | async deleteEdges(edges: Edge[]): Promise { 60 | return this.graphManager.deleteEdges(edges); 61 | } 62 | 63 | async getEdges(filter?: EdgeFilter): Promise { 64 | return this.graphManager.getEdges(filter); 65 | } 66 | 67 | async addMetadata(metadata: MetadataAddition[]): Promise { 68 | return this.graphManager.addMetadata(metadata); 69 | } 70 | 71 | async deleteMetadata(deletions: MetadataDeletion[]): Promise { 72 | return this.graphManager.deleteMetadata(deletions); 73 | } 74 | 75 | // Search operations delegated to SearchManager 76 | async readGraph(): Promise { 77 | return this.searchManager.readGraph(); 78 | } 79 | 80 | async searchNodes(query: string): Promise { 81 | return this.searchManager.searchNodes(query); 82 | } 83 | 84 | async openNodes(names: string[]): Promise { 85 | return this.searchManager.openNodes(names); 86 | } 87 | 88 | // Transaction operations delegated to TransactionManager 89 | async beginTransaction(): Promise { 90 | return this.transactionManager.beginTransaction(); 91 | } 92 | 93 | async commit(): Promise { 94 | return this.transactionManager.commit(); 95 | } 96 | 97 | async rollback(): Promise { 98 | return this.transactionManager.rollback(); 99 | } 100 | 101 | async withTransaction(operation: () => Promise): Promise { 102 | return this.transactionManager.withTransaction(operation); 103 | } 104 | 105 | async addRollbackAction(action: () => Promise, description: string): Promise { 106 | return this.transactionManager.addRollbackAction(action, description); 107 | } 108 | 109 | isInTransaction(): boolean { 110 | return this.transactionManager.isInTransaction(); 111 | } 112 | 113 | getCurrentGraph(): Graph { 114 | return this.transactionManager.getCurrentGraph(); 115 | } 116 | } -------------------------------------------------------------------------------- /src/application/managers/EdgeManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/EdgeManager.ts 2 | 3 | import {IEdgeManager} from '@application/managers/interfaces/IEdgeManager.js'; 4 | import {GraphValidator, EdgeWeightUtils} from '@core/index.js'; 5 | import type {Edge} from '@core/index.js'; 6 | import type {EdgeUpdate, EdgeFilter} from '@shared/index.js'; 7 | 8 | /** 9 | * Implements edge-related operations for the knowledge graph. 10 | * Includes adding, updating, deleting, and retrieving edges. 11 | */ 12 | export class EdgeManager extends IEdgeManager { 13 | /** 14 | * Adds new edges to the knowledge graph. 15 | */ 16 | async addEdges(edges: Edge[]): Promise { 17 | try { 18 | this.emit('beforeAddEdges', {edges}); 19 | 20 | const graph = await this.storage.loadGraph(); 21 | 22 | // Validate edge uniqueness and node existence using GraphValidator 23 | const newEdges = edges.filter(edge => { 24 | GraphValidator.validateEdgeUniqueness(graph, edge); 25 | // Ensure weights are set 26 | return EdgeWeightUtils.ensureWeight(edge); 27 | }); 28 | 29 | if (newEdges.length === 0) { 30 | return []; 31 | } 32 | 33 | for (const edge of newEdges) { 34 | GraphValidator.validateNodeExists(graph, edge.from); 35 | GraphValidator.validateNodeExists(graph, edge.to); 36 | } 37 | 38 | graph.edges.push(...newEdges); 39 | await this.storage.saveGraph(graph); 40 | 41 | this.emit('afterAddEdges', {edges: newEdges}); 42 | return newEdges; 43 | } catch (error) { 44 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 45 | throw new Error(errorMessage); 46 | } 47 | } 48 | 49 | /** 50 | * Updates existing edges in the knowledge graph. 51 | */ 52 | async updateEdges(edges: EdgeUpdate[]): Promise { 53 | try { 54 | this.emit('beforeUpdateEdges', {edges}); 55 | 56 | const graph = await this.storage.loadGraph(); 57 | const updatedEdges: Edge[] = []; 58 | 59 | for (const updateEdge of edges) { 60 | const edgeIndex = graph.edges.findIndex(existing => 61 | existing.from === updateEdge.from && 62 | existing.to === updateEdge.to && 63 | existing.edgeType === updateEdge.edgeType 64 | ); 65 | 66 | if (edgeIndex === -1) { 67 | throw new Error(`Edge not found: ${updateEdge.from} -> ${updateEdge.to} (${updateEdge.edgeType})`); 68 | } 69 | 70 | // Validate node existence for updated nodes using GraphValidator 71 | if (updateEdge.newFrom) { 72 | GraphValidator.validateNodeExists(graph, updateEdge.newFrom); 73 | } 74 | if (updateEdge.newTo) { 75 | GraphValidator.validateNodeExists(graph, updateEdge.newTo); 76 | } 77 | 78 | const updatedEdge: Edge = { 79 | type: 'edge', 80 | from: updateEdge.newFrom || graph.edges[edgeIndex].from, 81 | to: updateEdge.newTo || graph.edges[edgeIndex].to, 82 | edgeType: updateEdge.newEdgeType || graph.edges[edgeIndex].edgeType, 83 | weight: updateEdge.newWeight !== undefined ? updateEdge.newWeight : graph.edges[edgeIndex].weight 84 | }; 85 | 86 | // Validate the new weight if it's being updated 87 | if (updatedEdge.weight !== undefined) { 88 | EdgeWeightUtils.validateWeight(updatedEdge.weight); 89 | } 90 | 91 | graph.edges[edgeIndex] = updatedEdge; 92 | updatedEdges.push(updatedEdge); 93 | } 94 | 95 | await this.storage.saveGraph(graph); 96 | 97 | this.emit('afterUpdateEdges', {edges: updatedEdges}); 98 | return updatedEdges; 99 | } catch (error) { 100 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 101 | throw new Error(errorMessage); 102 | } 103 | } 104 | 105 | /** 106 | * Deletes edges from the knowledge graph. 107 | */ 108 | async deleteEdges(edges: Edge[]): Promise { 109 | try { 110 | this.emit('beforeDeleteEdges', {edges}); 111 | 112 | const graph = await this.storage.loadGraph(); 113 | const initialEdgeCount = graph.edges.length; 114 | 115 | graph.edges = graph.edges.filter(existing => 116 | !edges.some(edge => 117 | existing.from === edge.from && 118 | existing.to === edge.to && 119 | existing.edgeType === edge.edgeType 120 | ) 121 | ); 122 | 123 | const deletedCount = initialEdgeCount - graph.edges.length; 124 | 125 | await this.storage.saveGraph(graph); 126 | 127 | this.emit('afterDeleteEdges', {deletedCount}); 128 | } catch (error) { 129 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 130 | throw new Error(errorMessage); 131 | } 132 | } 133 | 134 | /** 135 | * Retrieves edges from the knowledge graph based on filter criteria. 136 | */ 137 | async getEdges(filter?: EdgeFilter): Promise { 138 | try { 139 | const graph = await this.storage.loadGraph(); 140 | 141 | if (!filter || Object.keys(filter).length === 0) { 142 | return graph.edges; 143 | } 144 | 145 | return graph.edges.filter(edge => { 146 | if (filter.from && edge.from !== filter.from) return false; 147 | if (filter.to && edge.to !== filter.to) return false; 148 | return !(filter.edgeType && edge.edgeType !== filter.edgeType); 149 | }); 150 | } catch (error) { 151 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 152 | throw new Error(errorMessage); 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /src/application/managers/GraphManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/GraphManager.ts 2 | 3 | import {BaseManager, GraphOperations} from '@application/index.js'; 4 | import type { 5 | MetadataAddition, 6 | MetadataDeletion, 7 | MetadataResult, 8 | Node, 9 | Edge 10 | } from '@core/index.js'; 11 | import type { 12 | EdgeFilter, 13 | GetEdgesResult, 14 | } from '@shared/index.js'; 15 | import type {IStorage} from '@infrastructure/index.js'; 16 | 17 | /** 18 | * Handles graph-specific operations (nodes, edges, metadata) 19 | */ 20 | export class GraphManager extends BaseManager { 21 | private readonly graphOperations: GraphOperations; 22 | 23 | constructor(storage?: IStorage) { 24 | super(storage); 25 | const {nodeManager, edgeManager, metadataManager} = this.createManagers(); 26 | this.graphOperations = new GraphOperations(nodeManager, edgeManager, metadataManager); 27 | } 28 | 29 | // Node operations 30 | async addNodes(nodes: Node[]): Promise { 31 | return this.graphOperations.addNodes(nodes); 32 | } 33 | 34 | async updateNodes(nodes: Partial[]): Promise { 35 | return this.graphOperations.updateNodes(nodes); 36 | } 37 | 38 | async deleteNodes(nodeNames: string[]): Promise { 39 | return this.graphOperations.deleteNodes(nodeNames); 40 | } 41 | 42 | // Edge operations 43 | async addEdges(edges: Edge[]): Promise { 44 | return this.graphOperations.addEdges(edges); 45 | } 46 | 47 | async updateEdges(edges: Edge[]): Promise { 48 | return this.graphOperations.updateEdges(edges); 49 | } 50 | 51 | async deleteEdges(edges: Edge[]): Promise { 52 | return this.graphOperations.deleteEdges(edges); 53 | } 54 | 55 | async getEdges(filter?: EdgeFilter): Promise { 56 | return this.graphOperations.getEdges(filter); 57 | } 58 | 59 | // Metadata operations 60 | async addMetadata(metadata: MetadataAddition[]): Promise { 61 | return this.graphOperations.addMetadata(metadata); 62 | } 63 | 64 | async deleteMetadata(deletions: MetadataDeletion[]): Promise { 65 | return this.graphOperations.deleteMetadata(deletions); 66 | } 67 | } -------------------------------------------------------------------------------- /src/application/managers/ManagerFactory.ts: -------------------------------------------------------------------------------- 1 | // src/core/managers/ManagerFactory.ts 2 | 3 | import { 4 | NodeManager, 5 | EdgeManager, 6 | MetadataManager, 7 | SearchManager, 8 | TransactionManager 9 | } from '@application/index.js'; 10 | import type {IStorage} from '@infrastructure/index.js'; 11 | import type { 12 | INodeManager, 13 | IEdgeManager, 14 | IMetadataManager, 15 | ISearchManager, 16 | ITransactionManager 17 | } from '@application/index.js'; 18 | 19 | /** 20 | * Factory class responsible for creating instances of various manager classes 21 | * used in the knowledge graph. Ensures consistent initialization and configuration 22 | * of all manager instances. 23 | */ 24 | export class ManagerFactory { 25 | private static instances: { 26 | nodeManager?: NodeManager; 27 | edgeManager?: EdgeManager; 28 | metadataManager?: MetadataManager; 29 | searchManager?: SearchManager; 30 | transactionManager?: TransactionManager; 31 | } = {}; 32 | 33 | /** 34 | * Creates or returns an existing instance of NodeManager 35 | */ 36 | static getNodeManager(storage: IStorage): INodeManager { 37 | if (!this.instances.nodeManager) { 38 | this.instances.nodeManager = new NodeManager(storage); 39 | } 40 | return this.instances.nodeManager; 41 | } 42 | 43 | /** 44 | * Creates or returns an existing instance of EdgeManager 45 | */ 46 | static getEdgeManager(storage: IStorage): IEdgeManager { 47 | if (!this.instances.edgeManager) { 48 | this.instances.edgeManager = new EdgeManager(storage); 49 | } 50 | return this.instances.edgeManager; 51 | } 52 | 53 | /** 54 | * Creates or returns an existing instance of MetadataManager 55 | */ 56 | static getMetadataManager(storage: IStorage): IMetadataManager { 57 | if (!this.instances.metadataManager) { 58 | this.instances.metadataManager = new MetadataManager(storage); 59 | } 60 | return this.instances.metadataManager; 61 | } 62 | 63 | /** 64 | * Creates or returns an existing instance of SearchManager 65 | */ 66 | static getSearchManager(storage: IStorage): ISearchManager { 67 | if (!this.instances.searchManager) { 68 | this.instances.searchManager = new SearchManager(storage); 69 | } 70 | return this.instances.searchManager; 71 | } 72 | 73 | /** 74 | * Creates or returns an existing instance of TransactionManager 75 | */ 76 | static getTransactionManager(storage: IStorage): ITransactionManager { 77 | if (!this.instances.transactionManager) { 78 | this.instances.transactionManager = new TransactionManager(storage); 79 | } 80 | return this.instances.transactionManager; 81 | } 82 | 83 | /** 84 | * Creates all manager instances at once 85 | */ 86 | static getAllManagers(storage: IStorage) { 87 | return { 88 | nodeManager: this.getNodeManager(storage), 89 | edgeManager: this.getEdgeManager(storage), 90 | metadataManager: this.getMetadataManager(storage), 91 | searchManager: this.getSearchManager(storage), 92 | transactionManager: this.getTransactionManager(storage) 93 | }; 94 | } 95 | 96 | /** 97 | * Clears all cached manager instances 98 | */ 99 | static clearInstances(): void { 100 | this.instances = {}; 101 | } 102 | } -------------------------------------------------------------------------------- /src/application/managers/MetadataManager.ts: -------------------------------------------------------------------------------- 1 | // src/core/managers/implementations/MetadataManager.ts 2 | 3 | import {IMetadataManager} from './interfaces/IMetadataManager.js'; 4 | import {IManager} from './interfaces/IManager.js'; 5 | import {GraphValidator} from '@core/index.js'; 6 | import type {Metadata, MetadataAddition, MetadataResult, MetadataDeletion} from '@core/index.js'; 7 | 8 | /** 9 | * Implements metadata-related operations for the knowledge graph. 10 | * Includes adding, deleting, and retrieving metadata associated with nodes. 11 | */ 12 | export class MetadataManager extends IManager implements IMetadataManager { 13 | /** 14 | * Adds metadata to existing nodes. 15 | */ 16 | async addMetadata(metadata: MetadataAddition[]): Promise { 17 | try { 18 | this.emit('beforeAddMetadata', {metadata}); 19 | 20 | const graph = await this.storage.loadGraph(); 21 | const results: MetadataResult[] = []; 22 | 23 | for (const item of metadata) { 24 | GraphValidator.validateNodeExists(graph, item.nodeName); 25 | const node = graph.nodes.find(e => e.name === item.nodeName); 26 | 27 | if (!Array.isArray(node!.metadata)) { 28 | node!.metadata = []; 29 | } 30 | 31 | const newMetadata = item.contents.filter(content => 32 | !node!.metadata.includes(content) 33 | ); 34 | 35 | node!.metadata.push(...newMetadata); 36 | results.push({ 37 | nodeName: item.nodeName, 38 | addedMetadata: newMetadata 39 | }); 40 | } 41 | 42 | await this.storage.saveGraph(graph); 43 | 44 | this.emit('afterAddMetadata', {results}); 45 | return results; 46 | } catch (error) { 47 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 48 | throw new Error(errorMessage); 49 | } 50 | } 51 | 52 | /** 53 | * Deletes metadata from nodes. 54 | */ 55 | async deleteMetadata(deletions: MetadataDeletion[]): Promise { 56 | try { 57 | this.emit('beforeDeleteMetadata', {deletions}); 58 | 59 | const graph = await this.storage.loadGraph(); 60 | let deletedCount = 0; 61 | 62 | for (const deletion of deletions) { 63 | GraphValidator.validateNodeExists(graph, deletion.nodeName); 64 | const node = graph.nodes.find(e => e.name === deletion.nodeName); 65 | 66 | if (node) { 67 | const initialMetadataCount = node.metadata.length; 68 | node.metadata = node.metadata.filter(o => 69 | !deletion.metadata.includes(o) 70 | ); 71 | deletedCount += initialMetadataCount - node.metadata.length; 72 | } 73 | } 74 | 75 | await this.storage.saveGraph(graph); 76 | 77 | this.emit('afterDeleteMetadata', {deletedCount}); 78 | } catch (error) { 79 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 80 | throw new Error(errorMessage); 81 | } 82 | } 83 | 84 | /** 85 | * Retrieves metadata for a specific node. 86 | */ 87 | async getMetadata(nodeName: string): Promise { 88 | try { 89 | const graph = await this.storage.loadGraph(); 90 | GraphValidator.validateNodeExists(graph, nodeName); 91 | const node = graph.nodes.find(e => e.name === nodeName); 92 | 93 | return node!.metadata || []; 94 | } catch (error) { 95 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 96 | throw new Error(errorMessage); 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/application/managers/NodeManager.ts: -------------------------------------------------------------------------------- 1 | // src/core/managers/implementations/NodeManager.ts 2 | 3 | import {IManager} from './interfaces/IManager.js'; 4 | import {INodeManager} from './interfaces/INodeManager.js'; 5 | import {GraphValidator} from '@core/index.js'; 6 | import type {Node} from '@core/index.js'; 7 | 8 | /** 9 | * Implements node-related operations for the knowledge graph. 10 | * Includes adding, updating, deleting, and retrieving nodes. 11 | */ 12 | export class NodeManager extends IManager implements INodeManager { 13 | /** 14 | * Adds new nodes to the knowledge graph. 15 | */ 16 | async addNodes(nodes: Node[]): Promise { 17 | try { 18 | this.emit('beforeAddNodes', {nodes}); 19 | 20 | const graph = await this.storage.loadGraph(); 21 | const newNodes: Node[] = []; 22 | 23 | for (const node of nodes) { 24 | GraphValidator.validateNodeProperties(node); 25 | GraphValidator.validateNodeDoesNotExist(graph, node.name); 26 | newNodes.push(node); 27 | } 28 | 29 | graph.nodes.push(...newNodes); 30 | await this.storage.saveGraph(graph); 31 | 32 | this.emit('afterAddNodes', {nodes: newNodes}); 33 | return newNodes; 34 | } catch (error) { 35 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 36 | throw new Error(errorMessage); 37 | } 38 | } 39 | 40 | /** 41 | * Updates existing nodes in the knowledge graph. 42 | */ 43 | async updateNodes(nodes: Partial[]): Promise { 44 | try { 45 | this.emit('beforeUpdateNodes', {nodes}); 46 | 47 | const graph = await this.storage.loadGraph(); 48 | const updatedNodes: Node[] = []; 49 | 50 | for (const updateNode of nodes) { 51 | GraphValidator.validateNodeNameProperty(updateNode); 52 | const nodeIndex = graph.nodes.findIndex(n => n.name === updateNode.name); 53 | 54 | if (nodeIndex === -1) { 55 | throw new Error(`Node not found: ${updateNode.name}`); 56 | } 57 | 58 | graph.nodes[nodeIndex] = { 59 | ...graph.nodes[nodeIndex], 60 | ...updateNode 61 | }; 62 | updatedNodes.push(graph.nodes[nodeIndex]); 63 | } 64 | 65 | await this.storage.saveGraph(graph); 66 | 67 | this.emit('afterUpdateNodes', {nodes: updatedNodes}); 68 | return updatedNodes; 69 | } catch (error) { 70 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 71 | throw new Error(errorMessage); 72 | } 73 | } 74 | 75 | /** 76 | * Deletes nodes and their associated edges from the knowledge graph. 77 | */ 78 | async deleteNodes(nodeNames: string[]): Promise { 79 | try { 80 | GraphValidator.validateNodeNamesArray(nodeNames); 81 | this.emit('beforeDeleteNodes', {nodeNames}); 82 | 83 | const graph = await this.storage.loadGraph(); 84 | const initialNodeCount = graph.nodes.length; 85 | 86 | graph.nodes = graph.nodes.filter(node => !nodeNames.includes(node.name)); 87 | graph.edges = graph.edges.filter(edge => 88 | !nodeNames.includes(edge.from) && !nodeNames.includes(edge.to) 89 | ); 90 | 91 | const deletedCount = initialNodeCount - graph.nodes.length; 92 | 93 | await this.storage.saveGraph(graph); 94 | 95 | this.emit('afterDeleteNodes', {deletedCount}); 96 | } catch (error) { 97 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 98 | throw new Error(errorMessage); 99 | } 100 | } 101 | 102 | /** 103 | * Retrieves specific nodes from the knowledge graph by their names. 104 | */ 105 | async getNodes(nodeNames: string[]): Promise { 106 | try { 107 | const graph = await this.storage.loadGraph(); 108 | return graph.nodes.filter(node => nodeNames.includes(node.name)); 109 | } catch (error) { 110 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 111 | throw new Error(errorMessage); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/application/managers/SearchManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/SearchManager.ts 2 | 3 | import {ISearchManager} from './interfaces/ISearchManager.js'; 4 | import {IManager} from './interfaces/IManager.js'; 5 | import type {Graph} from '@core/index.js'; 6 | 7 | /** 8 | * Implements search-related operations for the knowledge graph. 9 | * Provides functionality for searching nodes and retrieving graph data. 10 | */ 11 | export class SearchManager extends IManager implements ISearchManager { 12 | /** 13 | * Searches for nodes in the knowledge graph based on a query. 14 | * Includes both matching nodes and their immediate neighbors. 15 | */ 16 | async searchNodes(query: string): Promise { 17 | try { 18 | this.emit('beforeSearch', {query}); 19 | 20 | const graph = await this.storage.loadGraph(); 21 | 22 | // Find directly matching nodes 23 | const matchingNodes = graph.nodes.filter(node => 24 | node.name.toLowerCase().includes(query.toLowerCase()) || 25 | node.nodeType.toLowerCase().includes(query.toLowerCase()) || 26 | node.metadata.some(meta => 27 | meta.toLowerCase().includes(query.toLowerCase()) 28 | ) 29 | ); 30 | 31 | // Get names of matching nodes for efficient lookup 32 | const matchingNodeNames = new Set(matchingNodes.map(node => node.name)); 33 | 34 | // Find all edges connected to matching nodes 35 | const connectedEdges = graph.edges.filter(edge => 36 | matchingNodeNames.has(edge.from) || matchingNodeNames.has(edge.to) 37 | ); 38 | 39 | // Get names of all neighbor nodes from the edges 40 | const neighborNodeNames = new Set(); 41 | connectedEdges.forEach(edge => { 42 | if (matchingNodeNames.has(edge.from)) { 43 | neighborNodeNames.add(edge.to); 44 | } 45 | if (matchingNodeNames.has(edge.to)) { 46 | neighborNodeNames.add(edge.from); 47 | } 48 | }); 49 | 50 | // Get all neighbor nodes 51 | const neighborNodes = graph.nodes.filter(node => 52 | !matchingNodeNames.has(node.name) && neighborNodeNames.has(node.name) 53 | ); 54 | 55 | // Combine matching nodes and their neighbors 56 | const resultNodes = [...matchingNodes, ...neighborNodes]; 57 | 58 | const result: Graph = { 59 | nodes: resultNodes, 60 | edges: connectedEdges 61 | }; 62 | 63 | this.emit('afterSearch', result); 64 | return result; 65 | } catch (error) { 66 | const message = error instanceof Error ? error.message : 'Unknown error occurred'; 67 | throw new Error(`Search operation failed: ${message}`); 68 | } 69 | } 70 | 71 | /** 72 | * Retrieves specific nodes and their immediate neighbors from the knowledge graph. 73 | */ 74 | async openNodes(names: string[]): Promise { 75 | try { 76 | this.emit('beforeOpenNodes', {names}); 77 | 78 | const graph = await this.storage.loadGraph(); 79 | 80 | // Get the requested nodes 81 | const requestedNodes = graph.nodes.filter(node => 82 | names.includes(node.name) 83 | ); 84 | 85 | // Get names of requested nodes for efficient lookup 86 | const requestedNodeNames = new Set(requestedNodes.map(node => node.name)); 87 | 88 | // Find all edges connected to requested nodes 89 | const connectedEdges = graph.edges.filter(edge => 90 | requestedNodeNames.has(edge.from) || requestedNodeNames.has(edge.to) 91 | ); 92 | 93 | // Get names of all neighbor nodes from the edges 94 | const neighborNodeNames = new Set(); 95 | connectedEdges.forEach(edge => { 96 | if (requestedNodeNames.has(edge.from)) { 97 | neighborNodeNames.add(edge.to); 98 | } 99 | if (requestedNodeNames.has(edge.to)) { 100 | neighborNodeNames.add(edge.from); 101 | } 102 | }); 103 | 104 | // Get all neighbor nodes 105 | const neighborNodes = graph.nodes.filter(node => 106 | !requestedNodeNames.has(node.name) && neighborNodeNames.has(node.name) 107 | ); 108 | 109 | // Combine requested nodes and their neighbors 110 | const resultNodes = [...requestedNodes, ...neighborNodes]; 111 | 112 | const result: Graph = { 113 | nodes: resultNodes, 114 | edges: connectedEdges 115 | }; 116 | 117 | this.emit('afterOpenNodes', result); 118 | return result; 119 | } catch (error) { 120 | const message = error instanceof Error ? error.message : 'Unknown error occurred'; 121 | throw new Error(`Failed to open nodes: ${message}`); 122 | } 123 | } 124 | 125 | /** 126 | * Reads and returns the entire knowledge graph. 127 | */ 128 | async readGraph(): Promise { 129 | try { 130 | this.emit('beforeReadGraph', {}); 131 | const graph = await this.storage.loadGraph(); 132 | this.emit('afterReadGraph', graph); 133 | return graph; 134 | } catch (error) { 135 | const message = error instanceof Error ? error.message : 'Unknown error occurred'; 136 | throw new Error(`Failed to read graph: ${message}`); 137 | } 138 | } 139 | 140 | /** 141 | * Initializes the search manager. 142 | */ 143 | async initialize(): Promise { 144 | try { 145 | await super.initialize(); 146 | // Add any search-specific initialization here 147 | } catch (error) { 148 | const message = error instanceof Error ? error.message : 'Unknown error occurred'; 149 | throw new Error(`Failed to initialize SearchManager: ${message}`); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/application/managers/TransactionManager.ts: -------------------------------------------------------------------------------- 1 | // src/core/managers/implementations/TransactionManager.ts 2 | 3 | import {ITransactionManager, RollbackAction} from './interfaces/ITransactionManager.js'; 4 | import { IManager } from './interfaces/IManager.js'; 5 | import type {Graph} from '@core/index.js'; 6 | import type {IStorage} from '@infrastructure/index.js'; 7 | 8 | /** 9 | * Implements transaction-related operations for the knowledge graph. 10 | * Handles transaction lifecycle, rollback actions, and maintaining transaction state. 11 | */ 12 | export class TransactionManager extends IManager implements ITransactionManager { 13 | private graph: Graph; 14 | private rollbackActions: RollbackAction[]; 15 | private inTransaction: boolean; 16 | 17 | constructor(storage: IStorage) { 18 | super(storage); 19 | this.graph = {nodes: [], edges: []}; 20 | this.rollbackActions = []; 21 | this.inTransaction = false; 22 | } 23 | 24 | /** 25 | * Begins a new transaction. 26 | * @throws Error if a transaction is already in progress 27 | */ 28 | async beginTransaction(): Promise { 29 | if (this.inTransaction) { 30 | throw new Error('Transaction already in progress'); 31 | } 32 | 33 | this.emit('beforeBeginTransaction', {}); 34 | 35 | try { 36 | // Load current state 37 | this.graph = await this.storage.loadGraph(); 38 | this.rollbackActions = []; 39 | this.inTransaction = true; 40 | 41 | this.emit('afterBeginTransaction', {}); 42 | } catch (error) { 43 | const message = error instanceof Error ? error.message : 'Unknown error occurred'; 44 | throw new Error(`Failed to begin transaction: ${message}`); 45 | } 46 | } 47 | 48 | /** 49 | * Adds a rollback action to be executed if the transaction is rolled back. 50 | * @throws Error if no transaction is in progress 51 | */ 52 | async addRollbackAction(action: () => Promise, description: string): Promise { 53 | if (!this.inTransaction) { 54 | throw new Error('No transaction in progress'); 55 | } 56 | 57 | this.rollbackActions.push({action, description}); 58 | } 59 | 60 | /** 61 | * Commits the current transaction. 62 | * @throws Error if no transaction is in progress 63 | */ 64 | async commit(): Promise { 65 | if (!this.inTransaction) { 66 | throw new Error('No transaction to commit'); 67 | } 68 | 69 | this.emit('beforeCommit', {}); 70 | 71 | try { 72 | // Clear the transaction state 73 | this.rollbackActions = []; 74 | this.inTransaction = false; 75 | 76 | this.emit('afterCommit', {}); 77 | } catch (error) { 78 | const message = error instanceof Error ? error.message : 'Unknown error occurred'; 79 | throw new Error(`Failed to commit transaction: ${message}`); 80 | } 81 | } 82 | 83 | /** 84 | * Rolls back the current transaction, executing all rollback actions in reverse order. 85 | * @throws Error if no transaction is in progress 86 | */ 87 | async rollback(): Promise { 88 | if (!this.inTransaction) { 89 | throw new Error('No transaction to rollback'); 90 | } 91 | 92 | this.emit('beforeRollback', {actions: this.rollbackActions}); 93 | 94 | try { 95 | // Execute rollback actions in reverse order 96 | for (const {action, description} of this.rollbackActions.reverse()) { 97 | try { 98 | await action(); 99 | } catch (error) { 100 | console.error(`Error during rollback action (${description}):`, error); 101 | // Continue with other rollbacks even if one fails 102 | } 103 | } 104 | 105 | // Clear the transaction state 106 | this.rollbackActions = []; 107 | this.inTransaction = false; 108 | 109 | this.emit('afterRollback', {}); 110 | } catch (error) { 111 | const message = error instanceof Error ? error.message : 'Unknown error occurred'; 112 | throw new Error(`Failed to rollback transaction: ${message}`); 113 | } 114 | } 115 | 116 | /** 117 | * Gets the current graph state within the transaction. 118 | */ 119 | getCurrentGraph(): Graph { 120 | return this.graph; 121 | } 122 | 123 | /** 124 | * Checks if a transaction is currently in progress. 125 | */ 126 | isInTransaction(): boolean { 127 | return this.inTransaction; 128 | } 129 | 130 | /** 131 | * Executes an operation within a transaction, handling commit and rollback automatically. 132 | */ 133 | async withTransaction(operation: () => Promise): Promise { 134 | await this.beginTransaction(); 135 | try { 136 | const result = await operation(); 137 | await this.commit(); 138 | return result; 139 | } catch (error) { 140 | await this.rollback(); 141 | throw error; 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/application/managers/base/BaseManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/base/BaseManager.ts 2 | 3 | import type {IStorage} from '@infrastructure/index.js'; 4 | import {JsonLineStorage} from '@infrastructure/index.js'; 5 | import {ManagerFactory} from '@application/index.js'; 6 | 7 | /** 8 | * Base class that handles initialization and common functionality 9 | */ 10 | export abstract class BaseManager { 11 | protected readonly storage: IStorage; 12 | 13 | constructor(storage: IStorage = new JsonLineStorage()) { 14 | this.storage = storage; 15 | } 16 | 17 | protected createManagers() { 18 | return { 19 | nodeManager: ManagerFactory.getNodeManager(this.storage), 20 | edgeManager: ManagerFactory.getEdgeManager(this.storage), 21 | metadataManager: ManagerFactory.getMetadataManager(this.storage), 22 | searchManager: ManagerFactory.getSearchManager(this.storage), 23 | transactionManager: ManagerFactory.getTransactionManager(this.storage) 24 | }; 25 | } 26 | } -------------------------------------------------------------------------------- /src/application/managers/index.ts: -------------------------------------------------------------------------------- 1 | export {BaseManager} from './base/BaseManager.js'; 2 | export {EdgeManager} from './EdgeManager.js'; 3 | export {GraphManager} from './GraphManager.js'; 4 | export {MetadataManager} from './MetadataManager.js'; 5 | export {NodeManager} from './NodeManager.js'; 6 | export {SearchManager} from './SearchManager.js'; 7 | export {TransactionManager} from './TransactionManager.js'; 8 | export {ManagerFactory} from './ManagerFactory.js'; -------------------------------------------------------------------------------- /src/application/managers/interfaces/IEdgeManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/interfaces/IEdgeManager.ts 2 | 3 | import {IManager} from './IManager.js'; 4 | import type {EdgeUpdate, EdgeFilter} from '@shared/index.js'; 5 | import type {Edge} from '@core/index.js' 6 | 7 | /** 8 | * Interface for edge-related operations in the knowledge graph. 9 | * Defines the contract for managing edges, including adding, updating, deleting, and retrieving edges. 10 | */ 11 | export abstract class IEdgeManager extends IManager { 12 | /** 13 | * Adds new edges to the knowledge graph. 14 | */ 15 | abstract addEdges(edges: Edge[]): Promise; 16 | 17 | /** 18 | * Updates existing edges in the knowledge graph. 19 | */ 20 | abstract updateEdges(edges: EdgeUpdate[]): Promise; 21 | 22 | /** 23 | * Deletes edges from the knowledge graph. 24 | */ 25 | abstract deleteEdges(edges: Edge[]): Promise; 26 | 27 | /** 28 | * Retrieves edges from the knowledge graph based on filter criteria. 29 | */ 30 | abstract getEdges(filter?: EdgeFilter): Promise; 31 | } -------------------------------------------------------------------------------- /src/application/managers/interfaces/IManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/interfaces/IManager.ts 2 | 3 | import {EventEmitter, IStorage} from '@infrastructure/index.js'; 4 | 5 | /** 6 | * Abstract base class for all manager interfaces. 7 | * Provides event emission capabilities and implements common initialization. 8 | */ 9 | export abstract class IManager extends EventEmitter { 10 | /** 11 | * The storage instance used by the manager. 12 | */ 13 | protected storage: IStorage; 14 | 15 | /** 16 | * Creates an instance of IManager. 17 | * @param storage - The storage mechanism to use for persisting the knowledge graph. 18 | * @throws {Error} If attempting to instantiate the abstract class directly. 19 | */ 20 | constructor(storage: IStorage) { 21 | super(); 22 | if (new.target === IManager) { 23 | throw new Error('IManager is an abstract class'); 24 | } 25 | this.storage = storage; 26 | } 27 | 28 | /** 29 | * Initializes the manager by emitting the 'initialized' event. 30 | * Common implementation for all manager classes. 31 | */ 32 | public async initialize(): Promise { 33 | this.emit('initialized', {manager: this.constructor.name}); 34 | } 35 | } -------------------------------------------------------------------------------- /src/application/managers/interfaces/IManagerOperations.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/interfaces/IManagerOperations.ts 2 | 3 | import type { 4 | Node, 5 | Edge, 6 | Metadata, 7 | Graph, 8 | MetadataAddition, 9 | MetadataResult, 10 | MetadataDeletion 11 | } from '@core/index.js'; 12 | import type { 13 | EdgeUpdate, 14 | EdgeFilter 15 | } from '@shared/index.js'; 16 | 17 | /** 18 | * Base interface for all manager operations 19 | */ 20 | export interface IManagerOperations { 21 | initialize(): Promise; 22 | } 23 | 24 | /** 25 | * Node manager specific operations 26 | */ 27 | export interface INodeOperations extends IManagerOperations { 28 | addNodes(nodes: Node[]): Promise; 29 | 30 | updateNodes(nodes: Partial[]): Promise; 31 | 32 | deleteNodes(nodeNames: string[]): Promise; 33 | 34 | getNodes(nodeNames: string[]): Promise; 35 | } 36 | 37 | /** 38 | * Edge manager specific operations 39 | */ 40 | export interface IEdgeOperations extends IManagerOperations { 41 | addEdges(edges: Edge[]): Promise; 42 | 43 | updateEdges(edges: EdgeUpdate[]): Promise; 44 | 45 | deleteEdges(edges: Edge[]): Promise; 46 | 47 | getEdges(filter?: EdgeFilter): Promise; 48 | } 49 | 50 | /** 51 | * Metadata manager specific operations 52 | */ 53 | export interface IMetadataOperations extends IManagerOperations { 54 | addMetadata(metadata: MetadataAddition[]): Promise; 55 | 56 | deleteMetadata(deletions: MetadataDeletion[]): Promise; 57 | 58 | getMetadata(nodeName: string): Promise; 59 | } 60 | 61 | /** 62 | * Search manager specific operations 63 | */ 64 | export interface ISearchOperations extends IManagerOperations { 65 | searchNodes(query: string): Promise; 66 | 67 | openNodes(names: string[]): Promise; 68 | 69 | readGraph(): Promise; 70 | } -------------------------------------------------------------------------------- /src/application/managers/interfaces/IMetadataManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/interfaces/IMetadataManager.ts 2 | 3 | import {IManager} from './IManager.js'; 4 | import type {Metadata, MetadataAddition, MetadataResult, MetadataDeletion} from '@core/index.js'; 5 | 6 | /** 7 | * Interface for metadata-related operations in the knowledge graph. 8 | * Defines the contract for managing metadata, including adding, deleting, and retrieving metadata for nodes. 9 | */ 10 | export interface IMetadataManager extends IManager { 11 | /** 12 | * Adds metadata to existing nodes. 13 | */ 14 | addMetadata(metadata: MetadataAddition[]): Promise; 15 | 16 | /** 17 | * Deletes metadata from nodes. 18 | */ 19 | deleteMetadata(deletions: MetadataDeletion[]): Promise; 20 | 21 | /** 22 | * Retrieves metadata for a specific node. 23 | */ 24 | getMetadata(nodeName: string): Promise; 25 | } -------------------------------------------------------------------------------- /src/application/managers/interfaces/INodeManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/interfaces/INodeManager.ts 2 | 3 | import {IManager} from './IManager.js'; 4 | import type {Node} from '@core/index.js'; 5 | 6 | /** 7 | * Interface for node-related operations in the knowledge graph. 8 | * Defines the contract for managing nodes, including adding, updating, deleting, and retrieving nodes. 9 | */ 10 | export interface INodeManager extends IManager { 11 | /** 12 | * Adds new nodes to the knowledge graph. 13 | */ 14 | addNodes(nodes: Node[]): Promise; 15 | 16 | /** 17 | * Updates existing nodes in the knowledge graph. 18 | */ 19 | updateNodes(nodes: Partial[]): Promise; 20 | 21 | /** 22 | * Deletes nodes from the knowledge graph. 23 | */ 24 | deleteNodes(nodeNames: string[]): Promise; 25 | 26 | /** 27 | * Retrieves specific nodes from the knowledge graph by their names. 28 | */ 29 | getNodes(nodeNames: string[]): Promise; 30 | } -------------------------------------------------------------------------------- /src/application/managers/interfaces/ISearchManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/interfaces/ISearchManager.ts 2 | 3 | import {IManager} from './IManager.js'; 4 | import type {Graph} from '@core/index.js'; 5 | 6 | /** 7 | * Interface for search-related operations in the knowledge graph. 8 | * Defines the contract for searching nodes based on queries and retrieving specific nodes. 9 | */ 10 | export interface ISearchManager extends IManager { 11 | /** 12 | * Searches for nodes in the knowledge graph based on a query. 13 | */ 14 | searchNodes(query: string): Promise; 15 | 16 | /** 17 | * Retrieves specific nodes from the knowledge graph by their names. 18 | */ 19 | openNodes(names: string[]): Promise; 20 | 21 | /** 22 | * Reads and returns the entire knowledge graph. 23 | */ 24 | readGraph(): Promise; 25 | } -------------------------------------------------------------------------------- /src/application/managers/interfaces/ITransactionManager.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/interfaces/ITransactionManager.ts 2 | 3 | import type {Graph} from '@core/index.js'; 4 | import {IManager} from './IManager.js'; 5 | 6 | export interface RollbackAction { 7 | action: () => Promise; 8 | description: string; 9 | } 10 | 11 | export abstract class ITransactionManager extends IManager { 12 | abstract beginTransaction(): Promise; 13 | 14 | abstract addRollbackAction(action: () => Promise, description: string): Promise; 15 | 16 | abstract commit(): Promise; 17 | 18 | abstract rollback(): Promise; 19 | 20 | abstract getCurrentGraph(): Graph; 21 | 22 | abstract isInTransaction(): boolean; 23 | } 24 | -------------------------------------------------------------------------------- /src/application/managers/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | // src/application/managers/interfaces/index.ts 2 | 3 | export {IManager} from './IManager.js'; 4 | export {INodeManager} from './INodeManager.js'; 5 | export {IEdgeManager} from './IEdgeManager.js'; 6 | export {IMetadataManager} from './IMetadataManager.js'; 7 | export {ISearchManager} from './ISearchManager.js'; 8 | export {ITransactionManager, RollbackAction} from './ITransactionManager.js'; 9 | export { 10 | IManagerOperations, 11 | INodeOperations, 12 | IEdgeOperations, 13 | IMetadataOperations, 14 | ISearchOperations 15 | } from './IManagerOperations.js'; -------------------------------------------------------------------------------- /src/application/operations/GraphOperations.ts: -------------------------------------------------------------------------------- 1 | // src/core/operations/GraphOperations.ts 2 | 3 | import {EventEmitter} from '@infrastructure/index.js'; 4 | import type { 5 | INodeManager, 6 | IEdgeManager, 7 | IMetadataManager 8 | } from '@application/index.js'; 9 | import type { 10 | Node, 11 | Edge, 12 | MetadataAddition, 13 | MetadataDeletion, 14 | MetadataResult 15 | } from '@core/index.js'; 16 | import type { 17 | EdgeFilter, 18 | GetEdgesResult 19 | } from '@shared/index.js'; 20 | 21 | export class GraphOperations extends EventEmitter { 22 | constructor( 23 | private nodeManager: INodeManager, 24 | private edgeManager: IEdgeManager, 25 | private metadataManager: IMetadataManager 26 | ) { 27 | super(); 28 | } 29 | 30 | async addNodes(nodes: Node[]): Promise { 31 | this.emit('beforeAddNodes', {nodes}); 32 | const result = await this.nodeManager.addNodes(nodes); 33 | this.emit('afterAddNodes', {nodes: result}); 34 | return result; 35 | } 36 | 37 | async updateNodes(nodes: Partial[]): Promise { 38 | this.emit('beforeUpdateNodes', {nodes}); 39 | const result = await this.nodeManager.updateNodes(nodes); 40 | this.emit('afterUpdateNodes', {nodes: result}); 41 | return result; 42 | } 43 | 44 | async deleteNodes(nodeNames: string[]): Promise { 45 | this.emit('beforeDeleteNodes', {nodeNames}); 46 | await this.nodeManager.deleteNodes(nodeNames); 47 | this.emit('afterDeleteNodes', {nodeNames}); 48 | } 49 | 50 | async addEdges(edges: Edge[]): Promise { 51 | this.emit('beforeAddEdges', {edges}); 52 | const result = await this.edgeManager.addEdges(edges); 53 | this.emit('afterAddEdges', {edges: result}); 54 | return result; 55 | } 56 | 57 | async updateEdges(edges: Edge[]): Promise { 58 | this.emit('beforeUpdateEdges', {edges}); 59 | const result = await this.edgeManager.updateEdges(edges); 60 | this.emit('afterUpdateEdges', {edges: result}); 61 | return result; 62 | } 63 | 64 | async deleteEdges(edges: Edge[]): Promise { 65 | this.emit('beforeDeleteEdges', {edges}); 66 | await this.edgeManager.deleteEdges(edges); 67 | this.emit('afterDeleteEdges', {edges}); 68 | } 69 | 70 | async getEdges(filter?: EdgeFilter): Promise { 71 | const edges = await this.edgeManager.getEdges(filter); 72 | return {edges}; 73 | } 74 | 75 | async addMetadata(metadata: MetadataAddition[]): Promise { 76 | this.emit('beforeAddMetadata', {metadata}); 77 | const result = await this.metadataManager.addMetadata(metadata); 78 | this.emit('afterAddMetadata', {results: result}); 79 | return result; 80 | } 81 | 82 | async deleteMetadata(deletions: MetadataDeletion[]): Promise { 83 | this.emit('beforeDeleteMetadata', {deletions}); 84 | await this.metadataManager.deleteMetadata(deletions); 85 | this.emit('afterDeleteMetadata', {deletions}); 86 | } 87 | } -------------------------------------------------------------------------------- /src/application/operations/SearchOperations.ts: -------------------------------------------------------------------------------- 1 | // src/core/operations/SearchOperations.ts 2 | 3 | import {EventEmitter} from '@infrastructure/index.js'; 4 | import type {ISearchManager} from '@application/index.js'; 5 | import type {OpenNodesResult} from '@shared/index.js'; 6 | import type {Graph} from '@core/index.js'; 7 | 8 | export class SearchOperations extends EventEmitter { 9 | constructor(private searchManager: ISearchManager) { 10 | super(); 11 | } 12 | 13 | async searchNodes(query: string): Promise { 14 | this.emit('beforeSearch', {query}); 15 | const result = await this.searchManager.searchNodes(query); 16 | this.emit('afterSearch', result); 17 | return result; 18 | } 19 | 20 | async openNodes(names: string[]): Promise { 21 | this.emit('beforeOpenNodes', {names}); 22 | const result = await this.searchManager.openNodes(names); 23 | this.emit('afterOpenNodes', result); 24 | return result; 25 | } 26 | 27 | async readGraph(): Promise { 28 | this.emit('beforeReadGraph', {}); 29 | const result = await this.searchManager.readGraph(); 30 | this.emit('afterReadGraph', result); 31 | return result; 32 | } 33 | } -------------------------------------------------------------------------------- /src/application/operations/TransactionOperations.ts: -------------------------------------------------------------------------------- 1 | // src/core/operations/TransactionOperations.ts 2 | 3 | import {EventEmitter} from '@infrastructure/index.js'; 4 | import type {ITransactionManager} from '@application/index.js'; 5 | import type {Graph} from '@core/index.js'; 6 | 7 | export class TransactionOperations extends EventEmitter { 8 | constructor(private transactionManager: ITransactionManager) { 9 | super(); 10 | } 11 | 12 | async beginTransaction(): Promise { 13 | this.emit('beforeBeginTransaction', {}); 14 | await this.transactionManager.beginTransaction(); 15 | this.emit('afterBeginTransaction', {}); 16 | } 17 | 18 | async commit(): Promise { 19 | this.emit('beforeCommit', {}); 20 | await this.transactionManager.commit(); 21 | this.emit('afterCommit', {}); 22 | } 23 | 24 | async rollback(): Promise { 25 | this.emit('beforeRollback', {}); 26 | await this.transactionManager.rollback(); 27 | this.emit('afterRollback', {}); 28 | } 29 | 30 | async withTransaction(operation: () => Promise): Promise { 31 | await this.beginTransaction(); 32 | try { 33 | const result = await operation(); 34 | await this.commit(); 35 | return result; 36 | } catch (error) { 37 | await this.rollback(); 38 | throw error; 39 | } 40 | } 41 | 42 | async addRollbackAction(action: () => Promise, description: string): Promise { 43 | await this.transactionManager.addRollbackAction(action, description); 44 | } 45 | 46 | isInTransaction(): boolean { 47 | return this.transactionManager.isInTransaction(); 48 | } 49 | 50 | getCurrentGraph(): Graph { 51 | return this.transactionManager.getCurrentGraph(); 52 | } 53 | } -------------------------------------------------------------------------------- /src/application/operations/index.ts: -------------------------------------------------------------------------------- 1 | // src/application/operations/index.ts 2 | 3 | export {GraphOperations} from './GraphOperations.js'; 4 | export {SearchOperations} from './SearchOperations.js'; 5 | export {TransactionOperations} from './TransactionOperations.js'; -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | // src/config/config.ts 2 | 3 | import path from 'path'; 4 | import {fileURLToPath} from 'url'; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | interface ServerConfig { 9 | NAME: string; 10 | VERSION: string; 11 | } 12 | 13 | interface PathsConfig { 14 | SCHEMAS_DIR: string; 15 | MEMORY_FILE: string; 16 | } 17 | 18 | interface SchemaConfig { 19 | SUPPORTED_VERSIONS: string[]; 20 | } 21 | 22 | interface Config { 23 | SERVER: ServerConfig; 24 | PATHS: PathsConfig; 25 | SCHEMA: SchemaConfig; 26 | } 27 | 28 | /** 29 | * Centralized configuration for MemoryMesh. 30 | */ 31 | export const CONFIG: Config = { 32 | SERVER: { 33 | NAME: 'memorymesh', 34 | VERSION: '0.2.8', 35 | }, 36 | 37 | PATHS: { 38 | /** Path to schema files directory. */ 39 | SCHEMAS_DIR: path.join(__dirname, '..', 'data', 'schemas'), 40 | /** Path to the memory JSON file. */ 41 | MEMORY_FILE: path.join(__dirname, '..', 'data', 'memory.json'), 42 | }, 43 | 44 | SCHEMA: { 45 | /** Supported schema versions (not yet implemented). */ 46 | SUPPORTED_VERSIONS: ['0.1', '0.2'], // TODO: Add schema versioning 47 | }, 48 | }; -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | // src/config/index.ts 2 | 3 | export {CONFIG} from './config.js'; -------------------------------------------------------------------------------- /src/core/graph/Edge.ts: -------------------------------------------------------------------------------- 1 | // src/core/graph/Edge.ts 2 | 3 | /** 4 | * Represents an edge connecting two nodes in the knowledge graph 5 | */ 6 | export interface Edge { 7 | type: 'edge'; 8 | from: string; 9 | to: string; 10 | edgeType: string; 11 | weight?: number; // Optional weight property (0-1 range) 12 | } -------------------------------------------------------------------------------- /src/core/graph/EdgeWeightUtils.ts: -------------------------------------------------------------------------------- 1 | // src/core/graph/EdgeWeightUtils.ts 2 | 3 | import type {Edge} from './Edge.js'; 4 | 5 | /** 6 | * Utility functions for working with edge weights in the knowledge graph 7 | */ 8 | export class EdgeWeightUtils { 9 | /** 10 | * Validates that a weight is within the valid range (0-1) 11 | */ 12 | static validateWeight(weight: number): void { 13 | if (weight < 0 || weight > 1) { 14 | throw new Error('Edge weight must be between 0 and 1'); 15 | } 16 | } 17 | 18 | /** 19 | * Sets a default weight for an edge if none is provided 20 | */ 21 | static ensureWeight(edge: Edge): Edge { 22 | if (edge.weight === undefined) { 23 | return { 24 | ...edge, 25 | weight: 1 // Default to maximum weight 26 | }; 27 | } 28 | return edge; 29 | } 30 | 31 | /** 32 | * Updates the weight of an edge based on new evidence 33 | * Uses a simple averaging approach 34 | */ 35 | static updateWeight(currentWeight: number, newEvidence: number): number { 36 | this.validateWeight(newEvidence); 37 | return (currentWeight + newEvidence) / 2; 38 | } 39 | 40 | /** 41 | * Combines multiple edge weights (e.g., for parallel edges) 42 | * Uses the maximum weight by default 43 | */ 44 | static combineWeights(weights: number[]): number { 45 | if (weights.length === 0) return 1; 46 | return Math.max(...weights); 47 | } 48 | } -------------------------------------------------------------------------------- /src/core/graph/Graph.ts: -------------------------------------------------------------------------------- 1 | // src/core/graph/Graph.ts 2 | 3 | import type {Node} from './Node.js'; 4 | import type {Edge} from './Edge.js'; 5 | 6 | /** 7 | * Represents the complete knowledge graph structure 8 | */ 9 | export interface Graph { 10 | nodes: Node[]; 11 | edges: Edge[]; 12 | } -------------------------------------------------------------------------------- /src/core/graph/GraphValidator.ts: -------------------------------------------------------------------------------- 1 | // src/core/graph/GraphValidator.ts 2 | 3 | import type {Graph} from './Graph.js'; 4 | import type {Node} from './Node.js'; 5 | import type {Edge} from './Edge.js'; 6 | import {EdgeWeightUtils} from './EdgeWeightUtils.js'; 7 | 8 | /** 9 | * Provides validation methods for graph operations 10 | */ 11 | export class GraphValidator { 12 | /** 13 | * Validates that a node with the given name exists in the graph. 14 | */ 15 | static validateNodeExists(graph: Graph, nodeName: string): void { 16 | if (!graph.nodes.some(node => node.name === nodeName)) { 17 | throw new Error(`Node not found: ${nodeName}`); 18 | } 19 | } 20 | 21 | /** 22 | * Validates that a node with the given name does not exist in the graph. 23 | */ 24 | static validateNodeDoesNotExist(graph: Graph, nodeName: string): void { 25 | if (graph.nodes.some(node => node.name === nodeName)) { 26 | throw new Error(`Node already exists: ${nodeName}. Consider updating existing node.`); 27 | } 28 | } 29 | 30 | /** 31 | * Validates that an edge is unique in the graph. 32 | */ 33 | static validateEdgeUniqueness(graph: Graph, edge: Edge): void { 34 | if (graph.edges.some(existing => 35 | existing.from === edge.from && 36 | existing.to === edge.to && 37 | existing.edgeType === edge.edgeType 38 | )) { 39 | throw new Error(`Edge already exists: ${edge.from} -> ${edge.to} (${edge.edgeType})`); 40 | } 41 | } 42 | 43 | /** 44 | * Validates that a node has required properties. 45 | */ 46 | static validateNodeProperties(node: Node): void { 47 | if (!node.name) { 48 | throw new Error("Node must have a 'name' property"); 49 | } 50 | if (!node.nodeType) { 51 | throw new Error("Node must have a 'nodeType' property"); 52 | } 53 | if (!Array.isArray(node.metadata)) { 54 | throw new Error("Node must have a 'metadata' array"); 55 | } 56 | } 57 | 58 | /** 59 | * Validates that a partial node update has a name property. 60 | */ 61 | static validateNodeNameProperty(node: Partial): void { 62 | if (!node.name) { 63 | throw new Error("Node must have a 'name' property for updating"); 64 | } 65 | } 66 | 67 | /** 68 | * Validates that the provided value is a valid array of node names. 69 | */ 70 | static validateNodeNamesArray(nodeNames: unknown): asserts nodeNames is string[] { 71 | if (!Array.isArray(nodeNames)) { 72 | throw new Error("nodeNames must be an array"); 73 | } 74 | if (nodeNames.some(name => typeof name !== 'string')) { 75 | throw new Error("All node names must be strings"); 76 | } 77 | } 78 | 79 | /** 80 | * Validates edge properties. 81 | */ 82 | static validateEdgeProperties(edge: Edge): void { 83 | if (!edge.from) { 84 | throw new Error("Edge must have a 'from' property"); 85 | } 86 | if (!edge.to) { 87 | throw new Error("Edge must have a 'to' property"); 88 | } 89 | if (!edge.edgeType) { 90 | throw new Error("Edge must have an 'edgeType' property"); 91 | } 92 | if (edge.weight !== undefined) { 93 | EdgeWeightUtils.validateWeight(edge.weight); 94 | } 95 | } 96 | 97 | /** 98 | * Validates that all referenced nodes in edges exist. 99 | */ 100 | static validateEdgeReferences(graph: Graph, edges: Edge[]): void { 101 | for (const edge of edges) { 102 | this.validateNodeExists(graph, edge.from); 103 | this.validateNodeExists(graph, edge.to); 104 | } 105 | } 106 | 107 | /** 108 | * Validates the entire graph structure. 109 | */ 110 | static validateGraphStructure(graph: Graph): void { 111 | if (!Array.isArray(graph.nodes)) { 112 | throw new Error("Graph must have a 'nodes' array"); 113 | } 114 | if (!Array.isArray(graph.edges)) { 115 | throw new Error("Graph must have an 'edges' array"); 116 | } 117 | 118 | // Validate all nodes 119 | graph.nodes.forEach(node => this.validateNodeProperties(node)); 120 | 121 | // Validate all edges 122 | graph.edges.forEach(edge => { 123 | this.validateEdgeProperties(edge); 124 | this.validateNodeExists(graph, edge.from); 125 | this.validateNodeExists(graph, edge.to); 126 | }); 127 | } 128 | } 129 | 130 | // Export convenience functions 131 | export const { 132 | validateNodeExists, 133 | validateNodeDoesNotExist, 134 | validateEdgeUniqueness, 135 | validateNodeProperties, 136 | validateNodeNameProperty, 137 | validateNodeNamesArray, 138 | validateEdgeProperties, 139 | validateEdgeReferences, 140 | validateGraphStructure 141 | } = GraphValidator; -------------------------------------------------------------------------------- /src/core/graph/Node.ts: -------------------------------------------------------------------------------- 1 | // src/core/graph/Node.ts 2 | 3 | import type {Metadata} from '@core/index.js'; 4 | 5 | /** 6 | * Represents a node in the knowledge graph 7 | */ 8 | export interface Node { 9 | type: 'node'; 10 | name: string; 11 | nodeType: string; 12 | metadata: Metadata; 13 | } -------------------------------------------------------------------------------- /src/core/graph/index.ts: -------------------------------------------------------------------------------- 1 | // src/core/graph/index.ts 2 | 3 | export type {Node} from './Node.js'; 4 | export type {Edge} from './Edge.js'; 5 | export type {Graph} from './Graph.js'; 6 | export {GraphValidator} from './GraphValidator.js'; 7 | export { EdgeWeightUtils } from './EdgeWeightUtils.js'; -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | // src/core/index.ts 2 | 3 | export * from './graph/index.js'; 4 | export * from './metadata/index.js'; 5 | export * from './schema/index.js'; -------------------------------------------------------------------------------- /src/core/metadata/Metadata.ts: -------------------------------------------------------------------------------- 1 | // src/core/metadata/Metadata.ts 2 | 3 | /** 4 | * Represents metadata information associated with a node 5 | * Each string in the array represents a metadata entry in the format "key: value" 6 | */ 7 | export type Metadata = string[]; 8 | 9 | export interface MetadataEntry { 10 | key: string; 11 | value: string; 12 | } 13 | 14 | export interface MetadataAddition { 15 | nodeName: string; 16 | contents: string[]; 17 | } 18 | 19 | export interface MetadataDeletion { 20 | nodeName: string; 21 | metadata: string[]; 22 | } 23 | 24 | export interface MetadataResult { 25 | nodeName: string; 26 | addedMetadata: string[]; 27 | } -------------------------------------------------------------------------------- /src/core/metadata/MetadataProcessor.ts: -------------------------------------------------------------------------------- 1 | // src/core/metadata/MetadataProcessor.ts 2 | 3 | import {MetadataEntry, Metadata} from './Metadata.js'; 4 | 5 | export class MetadataProcessor { 6 | /** 7 | * Converts a raw metadata string to a structured entry 8 | */ 9 | static parseMetadataEntry(entry: string): MetadataEntry { 10 | const colonIndex = entry.indexOf(':'); 11 | if (colonIndex === -1) { 12 | throw new Error(`Invalid metadata format: ${entry}`); 13 | } 14 | 15 | return { 16 | key: entry.substring(0, colonIndex).trim(), 17 | value: entry.substring(colonIndex + 1).trim() 18 | }; 19 | } 20 | 21 | /** 22 | * Formats a metadata entry into a string 23 | */ 24 | static formatMetadataEntry(key: string, value: string | string[] | unknown): string { 25 | if (Array.isArray(value)) { 26 | return `${key}: ${value.join(', ')}`; 27 | } 28 | return `${key}: ${String(value)}`; 29 | } 30 | 31 | /** 32 | * Processes and validates metadata entries 33 | */ 34 | static validateMetadata(metadata: Metadata): boolean { 35 | try { 36 | metadata.forEach(entry => this.parseMetadataEntry(entry)); 37 | return true; 38 | } catch (error) { 39 | return false; 40 | } 41 | } 42 | 43 | /** 44 | * Merges multiple metadata arrays, removing duplicates 45 | */ 46 | static mergeMetadata(...metadataArrays: Metadata[]): Metadata { 47 | const uniqueEntries = new Set(); 48 | 49 | metadataArrays.forEach(metadata => { 50 | metadata.forEach(entry => uniqueEntries.add(entry)); 51 | }); 52 | 53 | return Array.from(uniqueEntries); 54 | } 55 | 56 | /** 57 | * Filters metadata entries by key 58 | */ 59 | static filterByKey(metadata: Metadata, key: string): Metadata { 60 | return metadata.filter(entry => { 61 | const parsed = this.parseMetadataEntry(entry); 62 | return parsed.key === key; 63 | }); 64 | } 65 | 66 | /** 67 | * Extracts value for a specific metadata key 68 | */ 69 | static getValue(metadata: Metadata, key: string): string | null { 70 | const entries = this.filterByKey(metadata, key); 71 | if (entries.length === 0) return null; 72 | 73 | return this.parseMetadataEntry(entries[0]).value; 74 | } 75 | 76 | /** 77 | * Creates a metadata entry map for efficient lookup 78 | */ 79 | static createMetadataMap(metadata: Metadata): Map { 80 | const map = new Map(); 81 | 82 | metadata.forEach(entry => { 83 | const {key, value} = this.parseMetadataEntry(entry); 84 | map.set(key, value); 85 | }); 86 | 87 | return map; 88 | } 89 | } -------------------------------------------------------------------------------- /src/core/metadata/index.ts: -------------------------------------------------------------------------------- 1 | // src/core/metadata/index.ts 2 | 3 | export type { 4 | Metadata, 5 | MetadataEntry, 6 | MetadataAddition, 7 | MetadataDeletion, 8 | MetadataResult 9 | } from './Metadata.js'; 10 | export {MetadataProcessor} from './MetadataProcessor.js'; -------------------------------------------------------------------------------- /src/core/schema/SchemaBuilder.ts: -------------------------------------------------------------------------------- 1 | // src/schema/loader/schemaBuilder.ts 2 | 3 | /** 4 | * Schema property configuration 5 | */ 6 | export interface SchemaPropertyConfig { 7 | type: string; 8 | description: string; 9 | enum?: string[]; 10 | items?: Partial; 11 | properties?: Record; 12 | required?: string[]; 13 | } 14 | 15 | /** 16 | * Relationship configuration 17 | */ 18 | export interface RelationshipConfig { 19 | edgeType: string; 20 | nodeType?: string; 21 | description?: string; 22 | } 23 | 24 | /** 25 | * Metadata configuration 26 | */ 27 | export interface MetadataConfig { 28 | requiredFields: string[]; 29 | optionalFields: string[]; 30 | excludeFields: string[]; 31 | } 32 | 33 | /** 34 | * Schema configuration 35 | */ 36 | export interface SchemaConfig { 37 | name: string; 38 | description: string; 39 | inputSchema: { 40 | type: string; 41 | properties: Record; 44 | required: string[]; 45 | additionalProperties: boolean | SchemaPropertyConfig; 46 | }>; 47 | required: string[]; 48 | }; 49 | relationships: Record; 50 | metadataConfig: MetadataConfig; 51 | } 52 | 53 | /** 54 | * Facilitates the construction and manipulation of schemas for nodes in the knowledge graph. 55 | */ 56 | export class SchemaBuilder { 57 | private schema: Partial; 58 | private relationships: Map; 59 | private metadataConfig: MetadataConfig; 60 | 61 | /** 62 | * Creates an instance of SchemaBuilder. 63 | */ 64 | constructor(name: string, description: string) { 65 | this.schema = { 66 | name, 67 | description, 68 | inputSchema: { 69 | type: "object", 70 | properties: { 71 | [name.replace('add_', '')]: { 72 | type: "object", 73 | properties: {}, 74 | required: [], 75 | additionalProperties: { 76 | type: "string", 77 | description: "Any additional properties" 78 | } 79 | } 80 | }, 81 | required: [name.replace('add_', '')] 82 | } 83 | }; 84 | 85 | this.relationships = new Map(); 86 | this.metadataConfig = { 87 | requiredFields: [], 88 | optionalFields: [], 89 | excludeFields: [] 90 | }; 91 | } 92 | 93 | /** 94 | * Adds a string property to the schema with an optional enum. 95 | */ 96 | addStringProperty( 97 | name: string, 98 | description: string, 99 | required: boolean = false, 100 | enumValues: string[] | null = null 101 | ): SchemaBuilder { 102 | const property: SchemaPropertyConfig = { 103 | type: "string", 104 | description 105 | }; 106 | 107 | if (enumValues) { 108 | property.enum = enumValues; 109 | } 110 | 111 | const schemaName = this.schema.name!.replace('add_', ''); 112 | if (this.schema.inputSchema?.properties[schemaName]) { 113 | this.schema.inputSchema.properties[schemaName].properties[name] = property; 114 | 115 | if (required) { 116 | this.schema.inputSchema.properties[schemaName].required.push(name); 117 | this.metadataConfig.requiredFields.push(name); 118 | } else { 119 | this.metadataConfig.optionalFields.push(name); 120 | } 121 | } 122 | 123 | return this; 124 | } 125 | 126 | /** 127 | * Adds an array property to the schema with optional enum values for items. 128 | */ 129 | addArrayProperty( 130 | name: string, 131 | description: string, 132 | required: boolean = false, 133 | enumValues: string[] | null = null 134 | ): SchemaBuilder { 135 | const property: SchemaPropertyConfig = { 136 | type: "array", 137 | description, 138 | items: { 139 | type: "string", 140 | description: `Item in ${name} array`, 141 | ...(enumValues && {enum: enumValues}) 142 | } 143 | }; 144 | 145 | const schemaName = this.schema.name!.replace('add_', ''); 146 | if (this.schema.inputSchema?.properties[schemaName]) { 147 | this.schema.inputSchema.properties[schemaName].properties[name] = property; 148 | 149 | if (required) { 150 | this.schema.inputSchema.properties[schemaName].required.push(name); 151 | this.metadataConfig.requiredFields.push(name); 152 | } else { 153 | this.metadataConfig.optionalFields.push(name); 154 | } 155 | } 156 | 157 | return this; 158 | } 159 | 160 | /** 161 | * Adds a relationship definition to the schema. 162 | */ 163 | addRelationship( 164 | propertyName: string, 165 | edgeType: string, 166 | description: string, 167 | nodeType: string | null = null 168 | ): SchemaBuilder { 169 | this.relationships.set(propertyName, { 170 | edgeType, 171 | ...(nodeType && {nodeType}), 172 | description 173 | }); 174 | this.metadataConfig.excludeFields.push(propertyName); 175 | return this.addArrayProperty(propertyName, description); 176 | } 177 | 178 | /** 179 | * Sets whether additional properties are allowed in the schema. 180 | */ 181 | allowAdditionalProperties(allowed: boolean): SchemaBuilder { 182 | const schemaName = this.schema.name!.replace('add_', ''); 183 | if (this.schema.inputSchema?.properties[schemaName]) { 184 | if (allowed) { 185 | this.schema.inputSchema.properties[schemaName].additionalProperties = { 186 | type: "string", 187 | description: "Additional property value" 188 | }; 189 | } else { 190 | this.schema.inputSchema.properties[schemaName].additionalProperties = false; 191 | } 192 | } 193 | return this; 194 | } 195 | 196 | /** 197 | * Creates an update schema based on the current schema. 198 | */ 199 | createUpdateSchema(excludeFields: Set = new Set()): SchemaConfig { 200 | const schemaName = this.schema.name!.replace('add_', 'update_'); 201 | const updateSchemaBuilder = new SchemaBuilder( 202 | schemaName, 203 | `Update an existing ${schemaName.replace('update_', '')} in the knowledge graph` 204 | ); 205 | 206 | const baseProperties = this.schema.inputSchema!.properties[this.schema.name!.replace('add_', '')].properties; 207 | 208 | // Copy properties except excluded ones 209 | Object.entries(baseProperties).forEach(([propName, propValue]) => { 210 | if (!excludeFields.has(propName)) { 211 | if (propValue.type === 'array') { 212 | updateSchemaBuilder.addArrayProperty( 213 | propName, 214 | propValue.description, 215 | false, 216 | propValue.items?.enum 217 | ); 218 | } else { 219 | updateSchemaBuilder.addStringProperty( 220 | propName, 221 | propValue.description, 222 | false, 223 | propValue.enum 224 | ); 225 | } 226 | } 227 | }); 228 | 229 | // Copy relationships 230 | this.relationships.forEach((config, propName) => { 231 | if (!excludeFields.has(propName)) { 232 | updateSchemaBuilder.addRelationship( 233 | propName, 234 | config.edgeType, 235 | config.description || 'Relationship property', 236 | config.nodeType || null 237 | ); 238 | } 239 | }); 240 | 241 | // Add metadata array 242 | updateSchemaBuilder.addArrayProperty( 243 | 'metadata', 244 | 'An array of metadata contents to replace the existing metadata' 245 | ); 246 | 247 | return updateSchemaBuilder.build(); 248 | } 249 | 250 | /** 251 | * Builds and returns the final schema object. 252 | */ 253 | build(): SchemaConfig { 254 | return { 255 | ...this.schema as SchemaConfig, 256 | relationships: Object.fromEntries(this.relationships), 257 | metadataConfig: this.metadataConfig 258 | }; 259 | } 260 | } -------------------------------------------------------------------------------- /src/core/schema/SchemaLoader.ts: -------------------------------------------------------------------------------- 1 | // src/schema/loader/schemaLoader.ts 2 | 3 | import {promises as fs} from 'fs'; 4 | import {SchemaBuilder} from './SchemaBuilder.js'; 5 | import path from 'path'; 6 | import {CONFIG} from '@config/index.js'; 7 | 8 | interface RawSchemaProperty { 9 | type: string; 10 | description: string; 11 | required?: boolean; 12 | enum?: string[]; 13 | relationship?: { 14 | edgeType: string; 15 | description: string; 16 | nodeType?: string; 17 | }; 18 | } 19 | 20 | interface RawSchema { 21 | name: string; 22 | description: string; 23 | properties: Record; 24 | additionalProperties?: boolean; 25 | } 26 | 27 | /** 28 | * Responsible for loading and converting schema definitions from JSON files into SchemaBuilder instances. 29 | */ 30 | export class SchemaLoader { 31 | /** 32 | * Loads a specific schema by name. 33 | */ 34 | static async loadSchema(schemaName: string): Promise { 35 | const SCHEMAS_DIR = CONFIG.PATHS.SCHEMAS_DIR; 36 | const schemaPath = path.join(SCHEMAS_DIR, `${schemaName}.schema.json`); 37 | 38 | try { 39 | const schemaContent = await fs.readFile(schemaPath, 'utf-8'); 40 | const schema = JSON.parse(schemaContent) as RawSchema; 41 | this.validateSchema(schema); 42 | return this.convertToSchemaBuilder(schema); 43 | } catch (error) { 44 | if (error instanceof Error) { 45 | throw new Error(`Failed to load schema ${schemaName}: ${error.message}`); 46 | } 47 | throw new Error(`Failed to load schema ${schemaName}`); 48 | } 49 | } 50 | 51 | /** 52 | * Converts a JSON schema object into a SchemaBuilder instance. 53 | */ 54 | static convertToSchemaBuilder(schema: RawSchema): SchemaBuilder { 55 | const builder = new SchemaBuilder(schema.name, schema.description); 56 | 57 | Object.entries(schema.properties).forEach(([propName, propConfig]) => { 58 | if (propConfig.type === 'array') { 59 | builder.addArrayProperty( 60 | propName, 61 | propConfig.description, 62 | propConfig.required, 63 | propConfig.enum 64 | ); 65 | } else { 66 | builder.addStringProperty( 67 | propName, 68 | propConfig.description, 69 | propConfig.required, 70 | propConfig.enum 71 | ); 72 | } 73 | 74 | // Add relationship if defined 75 | if (propConfig.relationship) { 76 | builder.addRelationship( 77 | propName, 78 | propConfig.relationship.edgeType, 79 | propConfig.relationship.description, 80 | propConfig.relationship.nodeType 81 | ); 82 | } 83 | }); 84 | 85 | if (schema.additionalProperties !== undefined) { 86 | builder.allowAdditionalProperties(schema.additionalProperties); 87 | } 88 | 89 | return builder; 90 | } 91 | 92 | /** 93 | * Loads all schema files from the schemas directory. 94 | */ 95 | static async loadAllSchemas(): Promise> { 96 | const SCHEMAS_DIR = CONFIG.PATHS.SCHEMAS_DIR; 97 | 98 | try { 99 | const files = await fs.readdir(SCHEMAS_DIR); 100 | const schemaFiles = files.filter(file => file.endsWith('.schema.json')); 101 | 102 | const schemas: Record = {}; 103 | for (const file of schemaFiles) { 104 | const schemaName = path.basename(file, '.schema.json'); 105 | schemas[schemaName] = await this.loadSchema(schemaName); 106 | } 107 | 108 | return schemas; 109 | } catch (error) { 110 | if (error instanceof Error) { 111 | throw new Error(`Failed to load schemas: ${error.message}`); 112 | } 113 | throw new Error('Failed to load schemas'); 114 | } 115 | } 116 | 117 | /** 118 | * Validates a schema definition. 119 | * @throws {Error} If the schema is invalid 120 | */ 121 | private static validateSchema(schema: RawSchema): void { 122 | if (!schema.name || !schema.description || !schema.properties) { 123 | throw new Error('Schema must have name, description, and properties'); 124 | } 125 | 126 | Object.entries(schema.properties).forEach(([propName, propConfig]) => { 127 | if (!propConfig.type || !propConfig.description) { 128 | throw new Error(`Property ${propName} must have type and description`); 129 | } 130 | 131 | if (propConfig.relationship && !propConfig.relationship.edgeType) { 132 | throw new Error(`Relationship property ${propName} must have edgeType`); 133 | } 134 | }); 135 | } 136 | } -------------------------------------------------------------------------------- /src/core/schema/SchemaProcessor.ts: -------------------------------------------------------------------------------- 1 | // src/schema/loader/schemaProcessor.ts 2 | 3 | import {formatToolResponse, formatToolError, ToolResponse} from '@shared/index.js'; 4 | import type {Node, Edge, Graph, SchemaConfig} from '@core/index.js'; 5 | import type {ApplicationManager} from '@application/index.js'; 6 | 7 | interface NodeData { 8 | name: string; 9 | 10 | [key: string]: any; 11 | } 12 | 13 | export interface ProcessedNodeResult { 14 | nodes: Node[]; 15 | edges: Edge[]; 16 | } 17 | 18 | export interface SchemaUpdateResult { 19 | metadata: string[]; 20 | edgeChanges: { 21 | remove: Edge[]; 22 | add: Edge[]; 23 | }; 24 | } 25 | 26 | /** 27 | * Formats a field value into a metadata string. 28 | */ 29 | function formatMetadataEntry(field: string, value: string | string[] | unknown): string { 30 | if (Array.isArray(value)) { 31 | return `${field}: ${value.join(', ')}`; 32 | } 33 | return `${field}: ${String(value)}`; 34 | } 35 | 36 | /** 37 | * Creates a node based on schema definition and input data. 38 | */ 39 | export async function createSchemaNode( 40 | data: NodeData, 41 | schema: SchemaConfig, 42 | nodeType: string 43 | ): Promise { 44 | try { 45 | const {metadataConfig, relationships} = schema; 46 | const metadata: string[] = []; 47 | const nodes: Node[] = []; 48 | const edges: Edge[] = []; 49 | 50 | // Create excluded fields set 51 | const excludedFields = new Set([ 52 | 'name', 53 | ...metadataConfig.requiredFields, 54 | ...metadataConfig.optionalFields, 55 | ...(metadataConfig.excludeFields || []), 56 | ]); 57 | 58 | if (relationships) { 59 | Object.keys(relationships).forEach(field => excludedFields.add(field)); 60 | } 61 | 62 | // Process required fields 63 | for (const field of metadataConfig.requiredFields) { 64 | if (data[field] === undefined) { 65 | throw new Error(`Required field "${field}" is missing`); 66 | } 67 | if (!relationships || !relationships[field]) { 68 | metadata.push(formatMetadataEntry(field, data[field])); 69 | } 70 | } 71 | 72 | // Process optional fields 73 | for (const field of metadataConfig.optionalFields) { 74 | if (data[field] !== undefined && (!relationships || !relationships[field])) { 75 | metadata.push(formatMetadataEntry(field, data[field])); 76 | } 77 | } 78 | 79 | // Process relationships 80 | if (relationships) { 81 | for (const [field, config] of Object.entries(relationships)) { 82 | if (data[field]) { 83 | const value = data[field]; 84 | if (Array.isArray(value)) { 85 | for (const target of value) { 86 | edges.push({ 87 | type: 'edge', 88 | from: data.name, 89 | to: target, 90 | edgeType: config.edgeType 91 | }); 92 | } 93 | } else { 94 | edges.push({ 95 | type: 'edge', 96 | from: data.name, 97 | to: value as string, 98 | edgeType: config.edgeType 99 | }); 100 | } 101 | metadata.push(formatMetadataEntry(field, value)); 102 | } 103 | } 104 | } 105 | 106 | // Process additional fields 107 | for (const [key, value] of Object.entries(data)) { 108 | if (!excludedFields.has(key) && value !== undefined) { 109 | metadata.push(formatMetadataEntry(key, value)); 110 | } 111 | } 112 | 113 | // Create the main node 114 | const node: Node = { 115 | type: 'node', 116 | name: data.name, 117 | nodeType, 118 | metadata 119 | }; 120 | nodes.push(node); 121 | 122 | return {nodes, edges}; 123 | } catch (error) { 124 | throw error; 125 | } 126 | } 127 | 128 | export async function updateSchemaNode( 129 | updates: NodeData, 130 | currentNode: Node, 131 | schema: SchemaConfig, 132 | currentGraph: Graph 133 | ): Promise { 134 | const {metadataConfig, relationships} = schema; 135 | const metadata = new Map(); 136 | const edgeChanges = { 137 | remove: [] as Edge[], 138 | add: [] as Edge[] 139 | }; 140 | 141 | // Create a set of all schema-defined fields 142 | const schemaFields = new Set([ 143 | ...metadataConfig.requiredFields, 144 | ...metadataConfig.optionalFields, 145 | ...(metadataConfig.excludeFields || []), 146 | 'name', 147 | 'metadata' 148 | ]); 149 | 150 | // Add relationship fields to schema fields 151 | if (relationships) { 152 | Object.keys(relationships).forEach(field => schemaFields.add(field)); 153 | } 154 | 155 | // Process existing metadata into the Map 156 | currentNode.metadata.forEach(meta => { 157 | const colonIndex = meta.indexOf(':'); 158 | if (colonIndex !== -1) { 159 | const key = meta.substring(0, colonIndex).trim().toLowerCase(); 160 | const value = meta.substring(colonIndex + 1).trim(); 161 | metadata.set(key, value); 162 | } 163 | }); 164 | 165 | const updateMetadataEntry = (key: string, value: unknown) => { 166 | const formattedValue = Array.isArray(value) ? value.join(', ') : String(value); 167 | metadata.set(key.toLowerCase(), formattedValue); 168 | }; 169 | 170 | // Process standard metadata fields 171 | const allSchemaFields = [...metadataConfig.requiredFields, ...metadataConfig.optionalFields]; 172 | for (const field of allSchemaFields) { 173 | if (updates[field] !== undefined && (!relationships || !relationships[field])) { 174 | updateMetadataEntry(field, updates[field]); 175 | } 176 | } 177 | 178 | // Process relationships if they exist in the schema 179 | if (relationships) { 180 | for (const [field, config] of Object.entries(relationships)) { 181 | // Only process relationship if it's being updated 182 | if (updates[field] !== undefined) { 183 | // Get all existing edges for this relationship type from this node 184 | const existingEdges = currentGraph.edges.filter(edge => 185 | edge.from === currentNode.name && 186 | edge.edgeType === config.edgeType 187 | ); 188 | 189 | // Only mark edges for removal if they're part of this relationship type 190 | edgeChanges.remove.push(...existingEdges); 191 | 192 | // Add new edges 193 | const value = updates[field]; 194 | if (Array.isArray(value)) { 195 | value.forEach((target: string) => { 196 | edgeChanges.add.push({ 197 | type: 'edge', 198 | from: currentNode.name, 199 | to: target, 200 | edgeType: config.edgeType 201 | }); 202 | }); 203 | } else if (value) { 204 | edgeChanges.add.push({ 205 | type: 'edge', 206 | from: currentNode.name, 207 | to: value as string, 208 | edgeType: config.edgeType 209 | }); 210 | } 211 | 212 | updateMetadataEntry(field, value); 213 | } 214 | } 215 | } 216 | 217 | // Process additional fields not defined in schema 218 | for (const [key, value] of Object.entries(updates)) { 219 | if (!schemaFields.has(key) && value !== undefined) { 220 | updateMetadataEntry(key, value); 221 | } 222 | } 223 | 224 | const updatedMetadata = Array.from(metadata).map(([key, value]) => { 225 | const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1); 226 | return `${capitalizedKey}: ${value}`; 227 | }); 228 | 229 | return { 230 | metadata: updatedMetadata, 231 | edgeChanges 232 | }; 233 | } 234 | 235 | /** 236 | * Handles the complete update process for a schema-based entity. 237 | */ 238 | export async function handleSchemaUpdate( 239 | updates: NodeData, 240 | schema: SchemaConfig, 241 | nodeType: string, 242 | applicationManager: ApplicationManager 243 | ): Promise { 244 | try { 245 | // Start a transaction to ensure atomic updates 246 | await applicationManager.beginTransaction(); 247 | 248 | // Get the complete current state 249 | const fullGraph = await applicationManager.readGraph(); 250 | const node = fullGraph.nodes.find((n: Node) => n.nodeType === nodeType && n.name === updates.name); 251 | 252 | if (!node) { 253 | await applicationManager.rollback(); 254 | return formatToolError({ 255 | operation: 'updateSchema', 256 | error: `${nodeType} "${updates.name}" not found`, 257 | context: {updates, nodeType}, 258 | suggestions: ["Verify the node exists", "Check node type matches"] 259 | }); 260 | } 261 | 262 | try { 263 | // Process updates 264 | const {metadata, edgeChanges} = await updateSchemaNode( 265 | updates, 266 | node, 267 | schema, 268 | fullGraph 269 | ); 270 | 271 | // Update the node first 272 | const updatedNode: Node = { 273 | ...node, 274 | metadata 275 | }; 276 | await applicationManager.updateNodes([updatedNode]); 277 | 278 | // Then handle edges if there are any changes 279 | if (edgeChanges.remove.length > 0) { 280 | await applicationManager.deleteEdges(edgeChanges.remove); 281 | } 282 | 283 | if (edgeChanges.add.length > 0) { 284 | await applicationManager.addEdges(edgeChanges.add); 285 | } 286 | 287 | // If everything succeeded, commit the transaction 288 | await applicationManager.commit(); 289 | 290 | return formatToolResponse({ 291 | data: { 292 | updatedNode, 293 | edgeChanges 294 | }, 295 | actionTaken: `Updated ${nodeType}: ${updatedNode.name}` 296 | }); 297 | 298 | } catch (error) { 299 | // If anything fails, rollback all changes 300 | await applicationManager.rollback(); 301 | throw error; 302 | } 303 | 304 | } catch (error) { 305 | if (applicationManager.isInTransaction()) { 306 | await applicationManager.rollback(); 307 | } 308 | 309 | return formatToolError({ 310 | operation: 'updateSchema', 311 | error: error instanceof Error ? error.message : 'Unknown error occurred', 312 | context: {updates, schema, nodeType}, 313 | suggestions: [ 314 | "Check all required fields are provided", 315 | "Verify relationship targets exist" 316 | ], 317 | recoverySteps: [ 318 | "Review schema requirements", 319 | "Ensure node exists before updating" 320 | ] 321 | }); 322 | } 323 | } 324 | 325 | export async function handleSchemaDelete( 326 | nodeName: string, 327 | nodeType: string, 328 | applicationManager: ApplicationManager 329 | ): Promise { 330 | try { 331 | const graph = await applicationManager.readGraph(); 332 | const node = graph.nodes.find((n: Node) => n.name === nodeName && n.nodeType === nodeType); 333 | 334 | if (!node) { 335 | return formatToolError({ 336 | operation: 'deleteSchema', 337 | error: `${nodeType} "${nodeName}" not found`, 338 | context: {nodeName, nodeType}, 339 | suggestions: ["Verify node name and type"] 340 | }); 341 | } 342 | 343 | await applicationManager.deleteNodes([nodeName]); 344 | 345 | return formatToolResponse({ 346 | actionTaken: `Deleted ${nodeType}: ${nodeName}` 347 | }); 348 | } catch (error) { 349 | return formatToolError({ 350 | operation: 'deleteSchema', 351 | error: error instanceof Error ? error.message : 'Unknown error occurred', 352 | context: {nodeName, nodeType}, 353 | suggestions: [ 354 | "Check node exists", 355 | "Verify delete permissions" 356 | ], 357 | recoverySteps: [ 358 | "Ensure no dependent nodes exist", 359 | "Try retrieving node first" 360 | ] 361 | }); 362 | } 363 | } -------------------------------------------------------------------------------- /src/core/schema/index.ts: -------------------------------------------------------------------------------- 1 | // src/core/schema/index.ts 2 | 3 | export { SchemaBuilder } from './SchemaBuilder.js'; 4 | export { SchemaLoader } from './SchemaLoader.js'; 5 | export { 6 | createSchemaNode, 7 | updateSchemaNode, 8 | handleSchemaUpdate, 9 | handleSchemaDelete 10 | } from './SchemaProcessor.js'; 11 | 12 | export type { 13 | SchemaPropertyConfig, 14 | RelationshipConfig, 15 | MetadataConfig, 16 | SchemaConfig 17 | } from './SchemaBuilder.js'; 18 | 19 | export type { 20 | ProcessedNodeResult, 21 | SchemaUpdateResult 22 | } from './SchemaProcessor.js'; -------------------------------------------------------------------------------- /src/data/memory.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CheMiguel23/MemoryMesh/db5a103b261e1fc6dd66e7ef39e4abd37ea1d510/src/data/memory.json -------------------------------------------------------------------------------- /src/data/schemas/artifact.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_artifact", 3 | "description": "Add a new artifact or unique item to the knowledge graph", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "The artifact's name", 8 | "required": true 9 | }, 10 | "description": { 11 | "type": "string", 12 | "description": "A detailed description of the artifact", 13 | "required": true 14 | }, 15 | "type": { 16 | "type": "string", 17 | "description": "The artifact's type", 18 | "required": true 19 | }, 20 | "rarity": { 21 | "type": "string", 22 | "description": "The rarity of the artifact", 23 | "required": true 24 | }, 25 | "effects": { 26 | "type": "array", 27 | "description": "The artifact's effects or abilities", 28 | "required": true 29 | }, 30 | "origin": { 31 | "type": "string", 32 | "description": "The artifact's origin or history", 33 | "required": false 34 | }, 35 | "value": { 36 | "type": "string", 37 | "description": "The monetary or intrinsic value of the artifact", 38 | "required": false 39 | }, 40 | "relatedCharacters": { 41 | "type": "array", 42 | "description": "Characters associated with the artifact", 43 | "required": false, 44 | "relationship": { 45 | "edgeType": "owned_by", 46 | "description": "Artifact owners" 47 | } 48 | }, 49 | "relatedQuests": { 50 | "type": "array", 51 | "description": "Quests involving the artifact", 52 | "required": false, 53 | "relationship": { 54 | "edgeType": "associated_with", 55 | "description": "Artifact-related quests" 56 | } 57 | }, 58 | "relatedLocations": { 59 | "type": "array", 60 | "description": "Locations associated with the artifact", 61 | "required": false, 62 | "relationship": { 63 | "edgeType": "found_at", 64 | "description": "Artifact locations" 65 | } 66 | } 67 | }, 68 | "additionalProperties": true 69 | } -------------------------------------------------------------------------------- /src/data/schemas/currency.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_currency", 3 | "description": "Represents a type of currency in the game world.", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "The name of the currency.", 8 | "required": true 9 | }, 10 | "description": { 11 | "type": "string", 12 | "description": "A brief description of the currency.", 13 | "required": false 14 | }, 15 | "owner": { 16 | "type": "string", 17 | "description": "The entity or character that owns this currency.", 18 | "required": true, 19 | "relationship": { 20 | "edgeType": "owned_by", 21 | "description": "The relationship between the currency and its owner." 22 | } 23 | }, 24 | "quantity": { 25 | "type": "string", 26 | "description": "The amount of this currency owned.", 27 | "required": true 28 | } 29 | }, 30 | "additionalProperties": true 31 | } -------------------------------------------------------------------------------- /src/data/schemas/faction.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_faction", 3 | "description": "A faction or organization operating within the game world.", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "The name of the faction or organization.", 8 | "required": true 9 | }, 10 | "type": { 11 | "type": "string", 12 | "description": "The type of the faction.", 13 | "required": true 14 | }, 15 | "description": { 16 | "type": "string", 17 | "description": "A detailed description of the faction.", 18 | "required": true 19 | }, 20 | "goals": { 21 | "type": "array", 22 | "description": "The main objectives or goals of the faction.", 23 | "required": false 24 | }, 25 | "leader": { 26 | "type": "string", 27 | "description": "The leader of the faction.", 28 | "required": false, 29 | "relationship": { 30 | "edgeType": "led_by", 31 | "description": "The entity leading this faction." 32 | } 33 | } 34 | }, 35 | "additionalProperties": true 36 | } -------------------------------------------------------------------------------- /src/data/schemas/inventory.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_inventory", 3 | "description": "A collection of items or equipment belonging to a character, entity, or location.", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "[Entity]_inventory.", 8 | "required": true 9 | }, 10 | "owner": { 11 | "type": "string", 12 | "description": "The owner of this inventory.", 13 | "required": true, 14 | "relationship": { 15 | "edgeType": "owned_by", 16 | "description": "The entity that owns this inventory." 17 | } 18 | }, 19 | "items": { 20 | "type": "array", 21 | "description": "List of items in the inventory.", 22 | "required": true 23 | } 24 | }, 25 | "additionalProperties": true 26 | } -------------------------------------------------------------------------------- /src/data/schemas/location.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_location", 3 | "description": "Add a new location to the knowledge graph", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "Location's name", 8 | "required": true 9 | }, 10 | "type": { 11 | "type": "string", 12 | "description": "Type of location", 13 | "required": true 14 | }, 15 | "description": { 16 | "type": "string", 17 | "description": "Detailed description of the location", 18 | "required": true 19 | }, 20 | "status": { 21 | "type": "string", 22 | "description": "Current state of the location", 23 | "required": true 24 | }, 25 | "parentLocation": { 26 | "type": "string", 27 | "description": "The parent location this location belongs to", 28 | "required": false 29 | }, 30 | "atmosphere": { 31 | "type": "string", 32 | "description": "The general feel/mood of the location", 33 | "required": false 34 | }, 35 | "accessibility": { 36 | "type": "string", 37 | "description": "How characters can enter/exit the location", 38 | "required": false 39 | }, 40 | "size": { 41 | "type": "string", 42 | "description": "The size or scale of the location", 43 | "required": false 44 | }, 45 | "dangerLevel": { 46 | "type": "string", 47 | "description": "The level of danger present in the location", 48 | "required": false 49 | }, 50 | "notableFeatures": { 51 | "type": "array", 52 | "description": "Distinct characteristics of the location", 53 | "required": false 54 | }, 55 | "subLocations": { 56 | "type": "array", 57 | "description": "Locations contained within this location", 58 | "required": false, 59 | "relationship": { 60 | "edgeType": "contains", 61 | "description": "Locations contained within this location" 62 | } 63 | }, 64 | "relatedCharacters": { 65 | "type": "array", 66 | "description": "Characters present in or associated with this location", 67 | "required": false, 68 | "relationship": { 69 | "edgeType": "present_in", 70 | "description": "Characters associated with the location" 71 | } 72 | }, 73 | "relatedQuests": { 74 | "type": "array", 75 | "description": "Quests associated with this location", 76 | "required": false, 77 | "relationship": { 78 | "edgeType": "takes_place_in", 79 | "description": "Quests associated with the location" 80 | } 81 | }, 82 | "relatedArtifacts": { 83 | "type": "array", 84 | "description": "Artifacts found or stored at this location", 85 | "required": false, 86 | "relationship": { 87 | "edgeType": "located_at", 88 | "description": "Artifacts associated with the location" 89 | } 90 | } 91 | }, 92 | "additionalProperties": true 93 | } -------------------------------------------------------------------------------- /src/data/schemas/npc.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_npc", 3 | "description": "Add a new Non-Player Character (NPC) to the knowledge graph", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "NPC's name", 8 | "required": true 9 | }, 10 | "role": { 11 | "type": "string", 12 | "description": "NPC's role or occupation", 13 | "required": true 14 | }, 15 | "status": { 16 | "type": "string", 17 | "description": "NPC's current status", 18 | "required": true 19 | }, 20 | "currentLocation": { 21 | "type": "string", 22 | "description": "The current location of the NPC", 23 | "required": true, 24 | "relationship": { 25 | "edgeType": "located_in", 26 | "description": "The current location of the NPC" 27 | } 28 | }, 29 | "description": { 30 | "type": "string", 31 | "description": "A detailed description of the NPC", 32 | "required": true 33 | }, 34 | "gender": { 35 | "type": "string", 36 | "description": "NPC's gender", 37 | "required": false 38 | }, 39 | "race": { 40 | "type": "string", 41 | "description": "NPC's race", 42 | "required": false 43 | }, 44 | "background": { 45 | "type": "string", 46 | "description": "The background story of the NPC", 47 | "required": false 48 | }, 49 | "secret": { 50 | "type": "string", 51 | "description": "A hidden detail about the NPC", 52 | "required": false 53 | }, 54 | "origin": { 55 | "type": "string", 56 | "description": "The origin or home location of the NPC", 57 | "required": false, 58 | "relationship": { 59 | "edgeType": "originates_from", 60 | "description": "The origin location of the NPC" 61 | } 62 | }, 63 | "traits": { 64 | "type": "array", 65 | "description": "Unique traits or characteristics of the NPC", 66 | "required": false 67 | }, 68 | "abilities": { 69 | "type": "array", 70 | "description": "Specific skills or powers the NPC possesses", 71 | "required": false 72 | }, 73 | "importance": { 74 | "type": "string", 75 | "description": "The importance of the NPC in the story or world", 76 | "required": false 77 | }, 78 | "reputation": { 79 | "type": "string", 80 | "description": "How the NPC is perceived by others", 81 | "required": false 82 | }, 83 | "money": { 84 | "type": "string", 85 | "description": "Currency or wealth the NPC holds", 86 | "required": false 87 | }, 88 | "alignment": { 89 | "type": "string", 90 | "description": "The ethical or moral alignment of the NPC", 91 | "required": false 92 | }, 93 | "motivation": { 94 | "type": "string", 95 | "description": "The driving purpose or goals of the NPC", 96 | "required": false 97 | } 98 | }, 99 | "additionalProperties": true 100 | } -------------------------------------------------------------------------------- /src/data/schemas/player_character.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_player_character", 3 | "description": "Add a new Player Character to the knowledge graph", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "Player character's name", 8 | "required": true 9 | }, 10 | "age": { 11 | "type": "string", 12 | "description": "Player character's age", 13 | "required": true 14 | }, 15 | "gender": { 16 | "type": "string", 17 | "description": "Player character's gender", 18 | "required": true 19 | }, 20 | "occupation": { 21 | "type": "string", 22 | "description": "Player character's occupation", 23 | "required": true 24 | }, 25 | "status": { 26 | "type": "string", 27 | "description": "Player character's current status", 28 | "required": true 29 | }, 30 | "race": { 31 | "type": "string", 32 | "description": "Player character's race", 33 | "required": false 34 | }, 35 | "description": { 36 | "type": "string", 37 | "description": "A detailed description of the player character", 38 | "required": false 39 | }, 40 | "background": { 41 | "type": "string", 42 | "description": "The background story of the player character", 43 | "required": false 44 | }, 45 | "equipment": { 46 | "type": "array", 47 | "description": "List of equipment items associated with the player character", 48 | "required": false 49 | } 50 | }, 51 | "additionalProperties": true 52 | } -------------------------------------------------------------------------------- /src/data/schemas/quest.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_quest", 3 | "description": "Add a new Quest to the knowledge graph", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "Quest's name", 8 | "required": true 9 | }, 10 | "description": { 11 | "type": "string", 12 | "description": "Detailed description of the quest", 13 | "required": true 14 | }, 15 | "status": { 16 | "type": "string", 17 | "description": "Current status of the quest", 18 | "required": true, 19 | "enum": [ 20 | "Active", 21 | "Completed", 22 | "Failed" 23 | ] 24 | }, 25 | "objectives": { 26 | "type": "array", 27 | "description": "List of objectives to complete the quest", 28 | "required": true, 29 | "items": { 30 | "type": "string" 31 | } 32 | }, 33 | "rewards": { 34 | "type": "array", 35 | "description": "List of rewards for completing the quest", 36 | "required": true, 37 | "items": { 38 | "type": "string" 39 | } 40 | }, 41 | "relatedCharacters": { 42 | "type": "array", 43 | "description": "List of player characters assigned to the quest", 44 | "required": false, 45 | "items": { 46 | "type": "string" 47 | } 48 | }, 49 | "relatedNPCs": { 50 | "type": "array", 51 | "description": "List of NPCs involved in the quest", 52 | "required": false, 53 | "items": { 54 | "type": "string" 55 | } 56 | }, 57 | "relatedLocations": { 58 | "type": "array", 59 | "description": "List of locations associated with the quest", 60 | "required": false, 61 | "items": { 62 | "type": "string" 63 | } 64 | } 65 | }, 66 | "additionalProperties": true 67 | } 68 | -------------------------------------------------------------------------------- /src/data/schemas/skills.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_skills", 3 | "description": "Defines list of skills or abilities a character can possess.", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "[Entity]_abilities", 8 | "required": true 9 | }, 10 | "owner": { 11 | "type": "string", 12 | "description": "The entity or character that owns these skills.", 13 | "required": true, 14 | "relationship": { 15 | "edgeType": "possesses", 16 | "description": "The relationship between the skill and its owner." 17 | } 18 | } 19 | }, 20 | "additionalProperties": true 21 | } 22 | -------------------------------------------------------------------------------- /src/data/schemas/temporal.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_temporal", 3 | "description": "Represents a specific point in time and its associated environmental conditions.", 4 | "properties": { 5 | "time": { 6 | "type": "string", 7 | "description": "The specific time.", 8 | "required": false 9 | }, 10 | "day": { 11 | "type": "string", 12 | "description": "The current day.", 13 | "required": false 14 | }, 15 | "year": { 16 | "type": "string", 17 | "description": "The current year or point in the timeline.", 18 | "required": false 19 | }, 20 | "weather": { 21 | "type": "string", 22 | "description": "The current weather or environmental conditions.", 23 | "required": false 24 | } 25 | }, 26 | "additionalProperties": true 27 | } 28 | -------------------------------------------------------------------------------- /src/data/schemas/transportation.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_transportation", 3 | "description": "Represents a transportation owned or used by a character or entity.", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "The name of the vehicle.", 8 | "required": true 9 | }, 10 | "description": { 11 | "type": "string", 12 | "description": "A brief description of the vehicle.", 13 | "required": true 14 | }, 15 | "owner": { 16 | "type": "string", 17 | "description": "The entity or character that owns this vehicle.", 18 | "required": true, 19 | "relationship": { 20 | "edgeType": "owned_by", 21 | "description": "The relationship between the vehicle and its owner." 22 | } 23 | }, 24 | "type": { 25 | "type": "string", 26 | "description": "The type or class of the vehicle (e.g., car, spaceship, boat, horse).", 27 | "required": true 28 | } 29 | }, 30 | "additionalProperties": true 31 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {Server} from "@modelcontextprotocol/sdk/server/index.js"; 3 | import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { 5 | CallToolRequestSchema, 6 | ListToolsRequestSchema, 7 | } from "@modelcontextprotocol/sdk/types.js"; 8 | import {ApplicationManager} from '@application/managers/ApplicationManager.js'; 9 | import {handleCallToolRequest} from '@integration/tools/callToolHandler.js'; 10 | import {toolsRegistry} from '@integration/tools/registry/toolsRegistry.js'; 11 | import {CONFIG} from './config/config.js'; 12 | import {formatToolError} from "@shared/utils/responseFormatter.js"; 13 | 14 | const knowledgeGraphManager = new ApplicationManager(); 15 | 16 | const server = new Server({ 17 | name: CONFIG.SERVER.NAME, 18 | version: CONFIG.SERVER.VERSION, 19 | }, { 20 | capabilities: { 21 | tools: {}, 22 | }, 23 | }); 24 | 25 | async function main(): Promise { 26 | try { 27 | await toolsRegistry.initialize(knowledgeGraphManager); 28 | 29 | server.setRequestHandler(ListToolsRequestSchema, async () => { 30 | return { 31 | tools: toolsRegistry.getAllTools().map(tool => ({ 32 | name: tool.name, 33 | description: tool.description, 34 | inputSchema: tool.inputSchema 35 | })) 36 | }; 37 | }); 38 | 39 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 40 | try { 41 | if (!request.params.arguments) { 42 | throw new Error("Tool arguments are required"); 43 | } 44 | 45 | const toolRequest = { 46 | params: { 47 | name: request.params.name, 48 | arguments: request.params.arguments 49 | } 50 | }; 51 | 52 | const result = await handleCallToolRequest(toolRequest, knowledgeGraphManager); 53 | 54 | return { 55 | toolResult: result.toolResult 56 | }; 57 | } catch (error) { 58 | console.error("Error in handleCallToolRequest:", error); 59 | const formattedError = formatToolError({ 60 | operation: "callTool", 61 | error: error instanceof Error ? error.message : 'Unknown error occurred', 62 | context: {request}, 63 | suggestions: ["Examine the tool input parameters for correctness.", "Verify that the requested operation is supported."], 64 | recoverySteps: ["Adjust the input parameters based on the schema definition."] 65 | }); 66 | return { 67 | toolResult: formattedError.toolResult 68 | }; 69 | } 70 | }); 71 | 72 | server.onerror = (error: Error) => { 73 | console.error("[MCP Server Error]", error); 74 | }; 75 | 76 | process.on('SIGINT', async () => { 77 | await server.close(); 78 | process.exit(0); 79 | }); 80 | 81 | const transport = new StdioServerTransport(); 82 | await server.connect(transport); 83 | console.error("Knowledge Graph MCP Server running on stdio"); 84 | } catch (error) { 85 | console.error("Fatal error during server startup:", error); 86 | process.exit(1); 87 | } 88 | } 89 | 90 | main().catch((error) => { 91 | console.error("Fatal error in main():", error); 92 | process.exit(1); 93 | }); -------------------------------------------------------------------------------- /src/infrastructure/events/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | // src/core/events/EventEmitter.ts 2 | 3 | import type {EventListener} from './EventTypes.ts'; 4 | 5 | /** 6 | * A simple event emitter implementation for managing event listeners and emitting events. 7 | */ 8 | export class EventEmitter { 9 | /** 10 | * Maps event names to their respective sets of listener functions. 11 | */ 12 | private eventListeners: Map>; 13 | 14 | constructor() { 15 | this.eventListeners = new Map(); 16 | 17 | // Bind methods to ensure correct 'this' context 18 | this.on = this.on.bind(this); 19 | this.off = this.off.bind(this); 20 | this.once = this.once.bind(this); 21 | this.emit = this.emit.bind(this); 22 | this.removeAllListeners = this.removeAllListeners.bind(this); 23 | } 24 | 25 | /** 26 | * Adds an event listener for the specified event. 27 | */ 28 | public on(eventName: string, listener: EventListener): () => void { 29 | if (typeof listener !== 'function') { 30 | throw new TypeError('Listener must be a function'); 31 | } 32 | 33 | if (!this.eventListeners.has(eventName)) { 34 | this.eventListeners.set(eventName, new Set()); 35 | } 36 | 37 | const listeners = this.eventListeners.get(eventName); 38 | if (listeners) { 39 | listeners.add(listener); 40 | } 41 | 42 | // Return unsubscribe function 43 | return () => this.off(eventName, listener); 44 | } 45 | 46 | /** 47 | * Removes an event listener for the specified event. 48 | */ 49 | public off(eventName: string, listener: EventListener): void { 50 | if (typeof listener !== 'function') { 51 | throw new TypeError('Listener must be a function'); 52 | } 53 | 54 | const listeners = this.eventListeners.get(eventName); 55 | if (listeners) { 56 | listeners.delete(listener); 57 | 58 | // Clean up empty listener sets 59 | if (listeners.size === 0) { 60 | this.eventListeners.delete(eventName); 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Adds a one-time event listener that removes itself after being called. 67 | */ 68 | public once(eventName: string, listener: EventListener): () => void { 69 | if (typeof listener !== 'function') { 70 | throw new TypeError('Listener must be a function'); 71 | } 72 | 73 | const onceWrapper: EventListener = (data: any) => { 74 | this.off(eventName, onceWrapper); 75 | listener(data); 76 | }; 77 | 78 | return this.on(eventName, onceWrapper); 79 | } 80 | 81 | /** 82 | * Emits an event with the specified data to all registered listeners. 83 | */ 84 | public emit(eventName: string, data?: any): boolean { 85 | 86 | const listeners = this.eventListeners.get(eventName); 87 | if (!listeners) { 88 | return false; 89 | } 90 | 91 | const errors: Error[] = []; 92 | listeners.forEach(listener => { 93 | try { 94 | listener(data); 95 | } catch (error) { 96 | errors.push(error instanceof Error ? error : new Error(String(error))); 97 | } 98 | }); 99 | 100 | if (errors.length > 0) { 101 | throw new Error(`Multiple errors occurred while emitting "${eventName}" event: ${errors.map(e => e.message).join(', ')}`); 102 | } 103 | 104 | return true; 105 | } 106 | 107 | /** 108 | * Removes all listeners for a specific event or all events. 109 | */ 110 | public removeAllListeners(eventName?: string): void { 111 | if (eventName === undefined) { 112 | this.eventListeners.clear(); 113 | } else { 114 | this.eventListeners.delete(eventName); 115 | } 116 | } 117 | 118 | /** 119 | * Gets the number of listeners for a specific event. 120 | */ 121 | public listenerCount(eventName: string): number { 122 | 123 | const listeners = this.eventListeners.get(eventName); 124 | return listeners ? listeners.size : 0; 125 | } 126 | 127 | /** 128 | * Gets all registered event names. 129 | */ 130 | public eventNames(): string[] { 131 | return Array.from(this.eventListeners.keys()); 132 | } 133 | 134 | /** 135 | * Gets all listeners for a specific event. 136 | */ 137 | public getListeners(eventName: string): EventListener[] { 138 | 139 | const listeners = this.eventListeners.get(eventName); 140 | return listeners ? Array.from(listeners) : []; 141 | } 142 | } -------------------------------------------------------------------------------- /src/infrastructure/events/EventTypes.ts: -------------------------------------------------------------------------------- 1 | // src/types/events.ts 2 | 3 | import { 4 | Edge, 5 | Graph, 6 | MetadataAddition, 7 | MetadataDeletion, 8 | MetadataResult 9 | } from "@core/index.js"; 10 | 11 | /** 12 | * Event handler function type 13 | */ 14 | export type EventListener = (data?: any) => void; 15 | 16 | /** 17 | * Map of event names to their listeners 18 | */ 19 | export interface EventMap { 20 | [eventName: string]: Set; 21 | } 22 | 23 | /** 24 | * Defines actions that can be rolled back during a transaction 25 | */ 26 | export interface RollbackAction { 27 | action: () => Promise; 28 | description: string; 29 | } 30 | 31 | /** 32 | * Event emitter events 33 | */ 34 | export interface EmitterEvents { 35 | // Manager Initialization 36 | initialized: { manager: string }; 37 | 38 | // Node Events 39 | beforeAddNodes: { nodes: Node[] }; 40 | afterAddNodes: { nodes: Node[] }; 41 | beforeUpdateNodes: { nodes: Node[] }; 42 | afterUpdateNodes: { nodes: Node[] }; 43 | beforeDeleteNodes: { nodeNames: string[] }; 44 | afterDeleteNodes: { deletedCount: number }; 45 | 46 | // Edge Events 47 | beforeAddEdges: { edges: Edge[] }; 48 | afterAddEdges: { edges: Edge[] }; 49 | beforeUpdateEdges: { edges: Edge[] }; 50 | afterUpdateEdges: { edges: Edge[] }; 51 | beforeDeleteEdges: { edges: Edge[] }; 52 | afterDeleteEdges: { deletedCount: number }; 53 | 54 | // Metadata Events 55 | beforeAddMetadata: { metadata: MetadataAddition[] }; 56 | afterAddMetadata: { results: MetadataResult[] }; 57 | beforeDeleteMetadata: { deletions: MetadataDeletion[] }; 58 | afterDeleteMetadata: { deletedCount: number }; 59 | 60 | // Search Events 61 | beforeSearch: { query: string }; 62 | afterSearch: Graph; 63 | beforeOpenNodes: { names: string[] }; 64 | afterOpenNodes: Graph; 65 | 66 | // Transaction Events 67 | beforeBeginTransaction: {}; 68 | afterBeginTransaction: {}; 69 | beforeCommit: {}; 70 | afterCommit: {}; 71 | beforeRollback: { actions: RollbackAction[] }; 72 | afterRollback: {}; 73 | 74 | // Error Event 75 | error: { operation: string; error: Error }; 76 | } -------------------------------------------------------------------------------- /src/infrastructure/events/index.ts: -------------------------------------------------------------------------------- 1 | // src/infrastructure/events/index.ts 2 | 3 | export {EventEmitter} from './EventEmitter.js'; 4 | export type { 5 | EventListener, 6 | EventMap, 7 | RollbackAction, 8 | EmitterEvents 9 | } from './EventTypes.js'; -------------------------------------------------------------------------------- /src/infrastructure/index.ts: -------------------------------------------------------------------------------- 1 | // src/infrastructure/index.ts 2 | 3 | export * from './events/index.js'; 4 | export * from './storage/index.js'; -------------------------------------------------------------------------------- /src/infrastructure/storage/IStorage.ts: -------------------------------------------------------------------------------- 1 | // src/types/storage.ts 2 | 3 | import type {Edge, Graph} from '@core/index.js'; 4 | 5 | /** 6 | * Edge indexing structure 7 | */ 8 | export interface EdgeIndex { 9 | byFrom: Map>; 10 | byTo: Map>; 11 | byType: Map>; 12 | } 13 | 14 | /** 15 | * Storage interface for graph operations 16 | */ 17 | export interface IStorage { 18 | loadGraph(): Promise; 19 | 20 | saveGraph(graph: Graph): Promise; 21 | 22 | loadEdgesByIds(edgeIds: string[]): Promise; 23 | } -------------------------------------------------------------------------------- /src/infrastructure/storage/JsonLineStorage.ts: -------------------------------------------------------------------------------- 1 | // src/core/storage/JsonLineStorage.ts 2 | 3 | import {promises as fs} from 'fs'; 4 | import path from 'path'; 5 | import {CONFIG} from '@config/config.js'; 6 | import type {IStorage} from './IStorage.js'; 7 | import type {Edge, Graph} from '@core/index.js'; 8 | 9 | /** 10 | * Handles persistent storage of the knowledge graph using a JSON Lines file format. 11 | */ 12 | export class JsonLineStorage implements IStorage { 13 | private edgeIndex: { 14 | byFrom: Map>; 15 | byTo: Map>; 16 | byType: Map>; 17 | }; 18 | private initialized: boolean; 19 | 20 | constructor() { 21 | this.edgeIndex = { 22 | byFrom: new Map(), 23 | byTo: new Map(), 24 | byType: new Map() 25 | }; 26 | this.initialized = false; 27 | } 28 | 29 | /** 30 | * Ensures the storage file and directory exist 31 | */ 32 | private async ensureStorageExists(): Promise { 33 | if (this.initialized) { 34 | return; 35 | } 36 | 37 | const MEMORY_FILE_PATH = CONFIG.PATHS.MEMORY_FILE; 38 | const dir = path.dirname(MEMORY_FILE_PATH); 39 | 40 | try { 41 | // Check if directory exists, create if it doesn't 42 | try { 43 | await fs.access(dir); 44 | } catch { 45 | await fs.mkdir(dir, {recursive: true}); 46 | } 47 | 48 | // Check if file exists, create if it doesn't 49 | try { 50 | await fs.access(MEMORY_FILE_PATH); 51 | } catch { 52 | await fs.writeFile(MEMORY_FILE_PATH, ''); 53 | } 54 | 55 | this.initialized = true; 56 | } catch (error) { 57 | console.error('Error initializing storage:', error); 58 | throw new Error('Failed to initialize storage'); 59 | } 60 | } 61 | 62 | /** 63 | * Loads the entire knowledge graph from storage and builds the edge indices. 64 | */ 65 | async loadGraph(): Promise { 66 | await this.ensureStorageExists(); 67 | 68 | try { 69 | const MEMORY_FILE_PATH = CONFIG.PATHS.MEMORY_FILE; 70 | const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); 71 | const lines = data.split("\n").filter(line => line.trim() !== ""); 72 | 73 | // Clear existing indices before rebuilding 74 | this.clearIndices(); 75 | 76 | const graph: Graph = {nodes: [], edges: []}; 77 | 78 | for (const line of lines) { 79 | try { 80 | const item = JSON.parse(line); 81 | if (item.type === "node") { 82 | graph.nodes.push(item); 83 | } else if (item.type === "edge") { 84 | graph.edges.push(item); 85 | } 86 | } catch (parseError) { 87 | console.error('Error parsing line:', line, parseError); 88 | } 89 | } 90 | 91 | return graph; 92 | } catch (error) { 93 | if (error instanceof Error && 'code' in error && error.code === "ENOENT") { 94 | return {nodes: [], edges: []}; 95 | } 96 | throw error; 97 | } 98 | } 99 | 100 | /** 101 | * Saves the entire knowledge graph to storage. 102 | */ 103 | async saveGraph(graph: Graph): Promise { 104 | await this.ensureStorageExists(); 105 | 106 | const MEMORY_FILE_PATH = CONFIG.PATHS.MEMORY_FILE; 107 | 108 | const processedEdges = graph.edges.map(edge => ({ 109 | ...edge, 110 | type: 'edge' 111 | })); 112 | 113 | const lines = [ 114 | ...graph.nodes.map(node => JSON.stringify({...node, type: 'node'})), 115 | ...processedEdges.map(edge => JSON.stringify(edge)) 116 | ]; 117 | 118 | await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n") + (lines.length > 0 ? "\n" : "")); 119 | } 120 | 121 | /** 122 | * Loads specific edges by their IDs from storage. 123 | */ 124 | async loadEdgesByIds(edgeIds: string[]): Promise { 125 | const graph = await this.loadGraph(); 126 | const edgeMap = new Map( 127 | graph.edges.map(edge => [this.generateEdgeId(edge), edge]) 128 | ); 129 | 130 | return edgeIds 131 | .map(id => edgeMap.get(id)) 132 | .filter((edge): edge is Edge => edge !== undefined); 133 | } 134 | 135 | /** 136 | * Indexes a single edge by adding it to all relevant indices. 137 | */ 138 | private indexEdge(edge: Edge): void { 139 | const edgeId = this.generateEdgeId(edge); 140 | 141 | // Index by 'from' node 142 | if (!this.edgeIndex.byFrom.has(edge.from)) { 143 | this.edgeIndex.byFrom.set(edge.from, new Set()); 144 | } 145 | this.edgeIndex.byFrom.get(edge.from)?.add(edgeId); 146 | 147 | // Index by 'to' node 148 | if (!this.edgeIndex.byTo.has(edge.to)) { 149 | this.edgeIndex.byTo.set(edge.to, new Set()); 150 | } 151 | this.edgeIndex.byTo.get(edge.to)?.add(edgeId); 152 | 153 | // Index by edge type 154 | if (!this.edgeIndex.byType.has(edge.edgeType)) { 155 | this.edgeIndex.byType.set(edge.edgeType, new Set()); 156 | } 157 | this.edgeIndex.byType.get(edge.edgeType)?.add(edgeId); 158 | } 159 | 160 | /** 161 | * Generates a unique ID for an edge based on its properties. 162 | */ 163 | private generateEdgeId(edge: Edge): string { 164 | return `${edge.from}|${edge.to}|${edge.edgeType}`; 165 | } 166 | 167 | /** 168 | * Clears all edge indices. 169 | */ 170 | private clearIndices(): void { 171 | this.edgeIndex.byFrom.clear(); 172 | this.edgeIndex.byTo.clear(); 173 | this.edgeIndex.byType.clear(); 174 | } 175 | } -------------------------------------------------------------------------------- /src/infrastructure/storage/index.ts: -------------------------------------------------------------------------------- 1 | // src/infrastructure/storage/index.ts 2 | 3 | export {JsonLineStorage} from './JsonLineStorage.js'; 4 | export type { 5 | IStorage, 6 | EdgeIndex 7 | } from './IStorage.js'; -------------------------------------------------------------------------------- /src/integration/index.ts: -------------------------------------------------------------------------------- 1 | // src/integration/index.ts 2 | 3 | export * from './tools/index.js'; -------------------------------------------------------------------------------- /src/integration/tools/DynamicSchemaToolRegistry.ts: -------------------------------------------------------------------------------- 1 | // src/tools/DynamicSchemaToolRegistry.ts 2 | 3 | import {promises as fs} from 'fs'; 4 | import path from 'path'; 5 | import { 6 | SchemaLoader, 7 | createSchemaNode, 8 | handleSchemaUpdate, 9 | handleSchemaDelete 10 | } from '@core/index.js'; 11 | import {CONFIG} from '@config/index.js'; 12 | import {formatToolResponse, formatToolError} from '@shared/index.js'; 13 | import type {ApplicationManager} from '@application/index.js'; 14 | import type {Tool, ToolResponse} from '@shared/index.js'; 15 | import type {SchemaBuilder} from '@core/index.js'; 16 | 17 | /** 18 | * Interface defining the public contract for dynamic schema tool registry 19 | */ 20 | export interface IDynamicSchemaToolRegistry { 21 | getTools(): Tool[]; 22 | 23 | handleToolCall(toolName: string, args: Record, knowledgeGraphManager: ApplicationManager): Promise; 24 | } 25 | 26 | /** 27 | * Manages dynamic tools generated from schema definitions 28 | */ 29 | class DynamicSchemaToolRegistry implements IDynamicSchemaToolRegistry { 30 | private schemas: Map; 31 | private toolsCache: Map; 32 | private static instance: DynamicSchemaToolRegistry; 33 | 34 | private constructor() { 35 | this.schemas = new Map(); 36 | this.toolsCache = new Map(); 37 | } 38 | 39 | /** 40 | * Gets the singleton instance 41 | */ 42 | public static getInstance(): DynamicSchemaToolRegistry { 43 | if (!DynamicSchemaToolRegistry.instance) { 44 | DynamicSchemaToolRegistry.instance = new DynamicSchemaToolRegistry(); 45 | } 46 | return DynamicSchemaToolRegistry.instance; 47 | } 48 | 49 | /** 50 | * Initializes the registry by loading schemas and generating tools 51 | */ 52 | public async initialize(): Promise { 53 | try { 54 | const SCHEMAS_DIR = CONFIG.PATHS.SCHEMAS_DIR; 55 | const schemaFiles = await fs.readdir(SCHEMAS_DIR); 56 | 57 | // Process schema files 58 | for (const file of schemaFiles) { 59 | if (file.endsWith('.schema.json')) { 60 | const schemaName = path.basename(file, '.schema.json'); 61 | const schema = await SchemaLoader.loadSchema(schemaName); 62 | this.schemas.set(schemaName, schema); 63 | } 64 | } 65 | 66 | // Generate tools for each schema 67 | for (const [schemaName, schema] of this.schemas.entries()) { 68 | const tools = await this.generateToolsForSchema(schemaName, schema); 69 | tools.forEach(tool => this.toolsCache.set(tool.name, tool)); 70 | } 71 | 72 | console.error(`[DynamicSchemaTools] Initialized ${this.schemas.size} schemas and ${this.toolsCache.size} tools`); 73 | } catch (error) { 74 | console.error('[DynamicSchemaTools] Initialization error:', error); 75 | throw error; 76 | } 77 | } 78 | 79 | /** 80 | * Retrieves all generated tools 81 | */ 82 | public getTools(): Tool[] { 83 | return Array.from(this.toolsCache.values()); 84 | } 85 | 86 | /** 87 | * Generates tools for a given schema 88 | */ 89 | private async generateToolsForSchema(schemaName: string, schema: SchemaBuilder): Promise { 90 | const tools: Tool[] = []; 91 | const baseSchema = schema.build(); 92 | 93 | // Add tool 94 | tools.push(baseSchema as unknown as Tool); 95 | 96 | // Update tool 97 | const updateSchema = schema.createUpdateSchema(); 98 | tools.push(updateSchema as unknown as Tool); 99 | 100 | // Delete tool 101 | const deleteSchema: Tool = { 102 | name: `delete_${schemaName}`, 103 | description: `Delete 104 | an existing 105 | ${schemaName} 106 | from 107 | the 108 | knowledge 109 | graph`, 110 | inputSchema: { 111 | type: "object", 112 | properties: { 113 | [`delete_${schemaName}`]: { 114 | type: "object", 115 | description: `Delete parameters for ${schemaName}`, 116 | properties: { 117 | name: { 118 | type: "string", 119 | description: `The name of the ${schemaName} to delete` 120 | } 121 | }, 122 | required: ["name"] 123 | } 124 | }, 125 | required: [`delete_${schemaName}`] 126 | } 127 | }; 128 | 129 | tools.push(deleteSchema); 130 | return tools; 131 | } 132 | 133 | /** 134 | * Handles tool calls for dynamically generated schema-based tools 135 | */ 136 | public async handleToolCall( 137 | toolName: string, 138 | args: Record, 139 | knowledgeGraphManager: ApplicationManager 140 | ): Promise { 141 | const match = toolName.match(/^(add|update|delete)_(.+)$/); 142 | if (!match) { 143 | return formatToolError({ 144 | operation: toolName, 145 | error: `Invalid tool name format: ${toolName}`, 146 | suggestions: ["Tool name must follow pattern: 'add|update|delete_'"] 147 | }); 148 | } 149 | 150 | const [, operation, schemaName] = match; 151 | const schemaBuilder = this.schemas.get(schemaName); 152 | 153 | if (!schemaBuilder) { 154 | return formatToolError({ 155 | operation: toolName, 156 | error: `Schema not found: ${schemaName}`, 157 | context: {availableSchemas: Array.from(this.schemas.keys())}, 158 | suggestions: ["Verify schema name exists"] 159 | }); 160 | } 161 | 162 | try { 163 | const schema = schemaBuilder.build(); 164 | 165 | switch (operation) { 166 | case 'add': { 167 | const nodeData = args[schemaName]; 168 | const existingNodes = await knowledgeGraphManager.openNodes([nodeData.name]); 169 | 170 | if (existingNodes.nodes.length > 0) { 171 | throw new Error(`Node already exists: ${nodeData.name}`); 172 | } 173 | 174 | const {nodes, edges} = await createSchemaNode(nodeData, schema, schemaName); 175 | 176 | await knowledgeGraphManager.beginTransaction(); 177 | try { 178 | await knowledgeGraphManager.addNodes(nodes); 179 | if (edges.length > 0) { 180 | await knowledgeGraphManager.addEdges(edges); 181 | } 182 | await knowledgeGraphManager.commit(); 183 | 184 | return formatToolResponse({ 185 | data: {nodes, edges}, 186 | actionTaken: `Created ${schemaName}: ${nodeData.name}` 187 | }); 188 | } catch (error) { 189 | await knowledgeGraphManager.rollback(); 190 | throw error; 191 | } 192 | } 193 | 194 | case 'update': { 195 | return handleSchemaUpdate( 196 | args[`update_${schemaName}`], 197 | schema, 198 | schemaName, 199 | knowledgeGraphManager 200 | ); 201 | } 202 | 203 | case 'delete': { 204 | const {name} = args[`delete_${schemaName}`]; 205 | if (!name) { 206 | return formatToolError({ 207 | operation: toolName, 208 | error: `Name is required to delete a ${schemaName}`, 209 | suggestions: ["Provide the 'name' parameter"] 210 | }); 211 | } 212 | return handleSchemaDelete(name, schemaName, knowledgeGraphManager); 213 | } 214 | 215 | default: 216 | return formatToolError({ 217 | operation: toolName, 218 | error: `Unknown operation: ${operation}`, 219 | suggestions: ["Use 'add', 'update', or 'delete'"] 220 | }); 221 | } 222 | } catch (error) { 223 | return formatToolError({ 224 | operation: toolName, 225 | error: error instanceof Error ? error.message : 'Unknown error occurred', 226 | context: {args}, 227 | suggestions: [ 228 | "Check input parameters against schema", 229 | "Verify entity existence for updates/deletes" 230 | ], 231 | recoverySteps: [ 232 | "Review schema requirements", 233 | "Ensure all required fields are provided" 234 | ] 235 | }); 236 | } 237 | } 238 | } 239 | 240 | // Create and export singleton instance 241 | export const dynamicSchemaTools = DynamicSchemaToolRegistry.getInstance(); 242 | 243 | /** 244 | * Initializes the dynamic tools registry 245 | */ 246 | export async function initializeDynamicTools(): Promise { 247 | await dynamicSchemaTools.initialize(); 248 | return dynamicSchemaTools; 249 | } -------------------------------------------------------------------------------- /src/integration/tools/callToolHandler.ts: -------------------------------------------------------------------------------- 1 | // src/tools/callToolHandler.ts 2 | 3 | import {toolsRegistry, ToolHandlerFactory} from '@integration/index.js'; 4 | import {formatToolError} from '@shared/index.js'; 5 | import type {ApplicationManager} from '@application/index.js'; 6 | import type {ToolResponse} from '@shared/index.js'; 7 | 8 | interface ToolCallRequest { 9 | params: { 10 | name: string; 11 | arguments: Record; 12 | }; 13 | } 14 | 15 | /** 16 | * Handles incoming tool call requests by routing them to the appropriate handler 17 | */ 18 | export async function handleCallToolRequest( 19 | request: ToolCallRequest, 20 | knowledgeGraphManager: ApplicationManager 21 | ): Promise { 22 | try { 23 | const {name, arguments: args} = request.params; 24 | 25 | if (!args) { 26 | return formatToolError({ 27 | operation: name, 28 | error: "Tool arguments are required", 29 | suggestions: ["Provide required arguments for the tool"] 30 | }); 31 | } 32 | 33 | // Ensure tools registry is initialized 34 | if (!toolsRegistry.hasTool(name)) { 35 | return formatToolError({ 36 | operation: name, 37 | error: `Tool not found: ${name}`, 38 | context: { 39 | availableTools: toolsRegistry.getAllTools().map(t => t.name) 40 | }, 41 | suggestions: ["Verify tool name is correct"], 42 | recoverySteps: ["Check available tools list"] 43 | }); 44 | } 45 | 46 | // Initialize handlers if needed 47 | if (!ToolHandlerFactory.isInitialized()) { 48 | ToolHandlerFactory.initialize(knowledgeGraphManager); 49 | } 50 | 51 | // Get appropriate handler and process the request 52 | const handler = ToolHandlerFactory.getHandler(name); 53 | return await handler.handleTool(name, args); 54 | 55 | } catch (error) { 56 | return formatToolError({ 57 | operation: "callTool", 58 | error: error instanceof Error ? error.message : 'Unknown error occurred', 59 | context: {request}, 60 | suggestions: [ 61 | "Verify tool name and arguments", 62 | "Check tool handler initialization" 63 | ], 64 | recoverySteps: [ 65 | "Review tool documentation", 66 | "Ensure all required arguments are provided" 67 | ] 68 | }); 69 | } 70 | } -------------------------------------------------------------------------------- /src/integration/tools/handlers/BaseToolHandler.ts: -------------------------------------------------------------------------------- 1 | // src/tools/handlers/BaseToolHandler.ts 2 | 3 | import {formatToolError} from '@shared/index.js'; 4 | import type {ApplicationManager} from '@application/index.js'; 5 | import type {ToolResponse} from '@shared/index.js'; 6 | 7 | export abstract class BaseToolHandler { 8 | constructor(protected knowledgeGraphManager: ApplicationManager) { 9 | } 10 | 11 | abstract handleTool(name: string, args: Record): Promise; 12 | 13 | protected validateArguments(args: Record): void { 14 | if (!args) { 15 | throw new Error("Tool arguments are required"); 16 | } 17 | } 18 | 19 | protected handleError(name: string, error: unknown): ToolResponse { 20 | console.error(`Error in ${name}:`, error); 21 | return formatToolError({ 22 | operation: name, 23 | error: error instanceof Error ? error.message : 'Unknown error occurred', 24 | context: {toolName: name}, 25 | suggestions: ["Examine the tool input parameters for correctness.", "Verify that the requested operation is supported."], 26 | recoverySteps: ["Adjust the input parameters based on the schema definition."] 27 | }); 28 | } 29 | } -------------------------------------------------------------------------------- /src/integration/tools/handlers/DynamicToolHandler.ts: -------------------------------------------------------------------------------- 1 | // src/tools/handlers/DynamicToolHandler.ts 2 | 3 | import {BaseToolHandler} from './BaseToolHandler.js'; 4 | import {dynamicToolManager} from '@integration/index.js'; 5 | import {formatToolResponse, formatToolError} from '@shared/index.js'; 6 | import type {ToolResponse} from '@shared/index.js'; 7 | 8 | export class DynamicToolHandler extends BaseToolHandler { 9 | async handleTool(name: string, args: Record): Promise { 10 | try { 11 | this.validateArguments(args); 12 | 13 | const toolResult = await dynamicToolManager.handleToolCall(name, args, this.knowledgeGraphManager); 14 | 15 | if (toolResult?.toolResult?.isError !== undefined) { 16 | return toolResult; 17 | } 18 | 19 | return formatToolResponse({ 20 | data: toolResult, 21 | actionTaken: `Executed dynamic tool: ${name}` 22 | }); 23 | } catch (error) { 24 | return formatToolError({ 25 | operation: name, 26 | error: error instanceof Error ? error.message : 'Unknown error occurred', 27 | context: {toolName: name, args}, 28 | suggestions: [ 29 | "Examine the tool input parameters for correctness", 30 | "Verify that the requested operation is supported" 31 | ], 32 | recoverySteps: [ 33 | "Adjust the input parameters based on the schema definition" 34 | ] 35 | }); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/integration/tools/handlers/GraphToolHandler.ts: -------------------------------------------------------------------------------- 1 | // src/tools/handlers/GraphToolHandler.ts 2 | 3 | import {BaseToolHandler} from './BaseToolHandler.js'; 4 | import {formatToolResponse, formatToolError} from '@shared/index.js'; 5 | import type {ToolResponse} from '@shared/index.js'; 6 | 7 | export class GraphToolHandler extends BaseToolHandler { 8 | async handleTool(name: string, args: Record): Promise { 9 | try { 10 | this.validateArguments(args); 11 | 12 | switch (name) { 13 | case "add_nodes": 14 | const addedNodes = await this.knowledgeGraphManager.addNodes(args.nodes); 15 | return formatToolResponse({ 16 | data: {nodes: addedNodes}, 17 | actionTaken: "Added nodes to knowledge graph" 18 | }); 19 | 20 | case "update_nodes": 21 | const updatedNodes = await this.knowledgeGraphManager.updateNodes(args.nodes); 22 | return formatToolResponse({ 23 | data: {nodes: updatedNodes}, 24 | actionTaken: "Updated nodes in knowledge graph" 25 | }); 26 | 27 | case "add_edges": 28 | const addedEdges = await this.knowledgeGraphManager.addEdges(args.edges); 29 | return formatToolResponse({ 30 | data: {edges: addedEdges}, 31 | actionTaken: "Added edges to knowledge graph" 32 | }); 33 | 34 | case "update_edges": 35 | const updatedEdges = await this.knowledgeGraphManager.updateEdges(args.edges); 36 | return formatToolResponse({ 37 | data: {edges: updatedEdges}, 38 | actionTaken: "Updated edges in knowledge graph" 39 | }); 40 | 41 | case "delete_nodes": 42 | await this.knowledgeGraphManager.deleteNodes(args.nodeNames); 43 | return formatToolResponse({ 44 | actionTaken: `Deleted nodes: ${args.nodeNames.join(', ')}` 45 | }); 46 | 47 | case "delete_edges": 48 | await this.knowledgeGraphManager.deleteEdges(args.edges); 49 | return formatToolResponse({ 50 | actionTaken: "Deleted edges from knowledge graph" 51 | }); 52 | 53 | default: 54 | throw new Error(`Unknown graph operation: ${name}`); 55 | } 56 | } catch (error) { 57 | return formatToolError({ 58 | operation: name, 59 | error: error instanceof Error ? error.message : 'Unknown error occurred', 60 | context: {args}, 61 | suggestions: [ 62 | "Verify node/edge existence", 63 | "Check input parameters format" 64 | ], 65 | recoverySteps: [ 66 | "Review the error details and adjust inputs", 67 | "Ensure referenced nodes exist before creating edges" 68 | ] 69 | }); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/integration/tools/handlers/MetadataToolHandler.ts: -------------------------------------------------------------------------------- 1 | // src/tools/handlers/MetadataToolHandler.ts 2 | 3 | import {BaseToolHandler} from './BaseToolHandler.js'; 4 | import {formatToolResponse, formatToolError} from '@shared/index.js'; 5 | import type {ToolResponse} from '@shared/index.js'; 6 | 7 | export class MetadataToolHandler extends BaseToolHandler { 8 | async handleTool(name: string, args: Record): Promise { 9 | try { 10 | this.validateArguments(args); 11 | 12 | switch (name) { 13 | case "add_metadata": 14 | const addResult = await this.knowledgeGraphManager.addMetadata(args.metadata); 15 | return formatToolResponse({ 16 | data: {metadata: addResult}, 17 | actionTaken: "Added metadata to nodes" 18 | }); 19 | 20 | case "delete_metadata": 21 | await this.knowledgeGraphManager.deleteMetadata(args.deletions); 22 | return formatToolResponse({ 23 | actionTaken: "Deleted metadata from nodes" 24 | }); 25 | 26 | default: 27 | throw new Error(`Unknown metadata operation: ${name}`); 28 | } 29 | } catch (error) { 30 | return formatToolError({ 31 | operation: name, 32 | error: error instanceof Error ? error.message : 'Unknown error occurred', 33 | context: {args}, 34 | suggestions: [ 35 | "Verify node existence", 36 | "Check metadata format" 37 | ], 38 | recoverySteps: [ 39 | "Ensure nodes exist before adding metadata", 40 | "Verify metadata content format" 41 | ] 42 | }); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/integration/tools/handlers/SearchToolHandler.ts: -------------------------------------------------------------------------------- 1 | // src/tools/handlers/SearchToolHandler.ts 2 | 3 | import {BaseToolHandler} from './BaseToolHandler.js'; 4 | import {formatToolResponse, formatToolError} from '@shared/index.js'; 5 | import type {ToolResponse} from '@shared/index.js'; 6 | 7 | export class SearchToolHandler extends BaseToolHandler { 8 | async handleTool(name: string, args: Record): Promise { 9 | try { 10 | this.validateArguments(args); 11 | 12 | switch (name) { 13 | case "read_graph": 14 | const graph = await this.knowledgeGraphManager.readGraph(); 15 | return formatToolResponse({ 16 | data: graph, 17 | actionTaken: "Read complete knowledge graph" 18 | }); 19 | 20 | case "search_nodes": 21 | const searchResults = await this.knowledgeGraphManager.searchNodes(args.query); 22 | return formatToolResponse({ 23 | data: searchResults, 24 | actionTaken: `Searched nodes with query: ${args.query}` 25 | }); 26 | 27 | case "open_nodes": 28 | const nodes = await this.knowledgeGraphManager.openNodes(args.names); 29 | return formatToolResponse({ 30 | data: nodes, 31 | actionTaken: `Retrieved nodes: ${args.names.join(', ')}` 32 | }); 33 | 34 | default: 35 | throw new Error(`Unknown search operation: ${name}`); 36 | } 37 | } catch (error) { 38 | return formatToolError({ 39 | operation: name, 40 | error: error instanceof Error ? error.message : 'Unknown error occurred', 41 | context: {args}, 42 | suggestions: [ 43 | "Check node names exist", 44 | "Verify search query format" 45 | ], 46 | recoverySteps: [ 47 | "Try with different node names", 48 | "Adjust search query parameters" 49 | ] 50 | }); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/integration/tools/handlers/ToolHandlerFactory.ts: -------------------------------------------------------------------------------- 1 | // src/tools/handlers/ToolHandlerFactory.ts 2 | 3 | import {GraphToolHandler} from './GraphToolHandler.js'; 4 | import {SearchToolHandler} from './SearchToolHandler.js'; 5 | import {MetadataToolHandler} from './MetadataToolHandler.js'; 6 | import {DynamicToolHandler} from './DynamicToolHandler.js'; 7 | import {toolsRegistry} from '@integration/index.js'; 8 | import type {ApplicationManager} from '@application/index.js'; 9 | import type {BaseToolHandler} from './BaseToolHandler.js'; 10 | 11 | export class ToolHandlerFactory { 12 | private static graphHandler: GraphToolHandler; 13 | private static searchHandler: SearchToolHandler; 14 | private static metadataHandler: MetadataToolHandler; 15 | private static dynamicHandler: DynamicToolHandler; 16 | private static initialized = false; 17 | 18 | /** 19 | * Initializes all tool handlers 20 | */ 21 | static initialize(knowledgeGraphManager: ApplicationManager): void { 22 | if (this.initialized) { 23 | return; 24 | } 25 | 26 | this.graphHandler = new GraphToolHandler(knowledgeGraphManager); 27 | this.searchHandler = new SearchToolHandler(knowledgeGraphManager); 28 | this.metadataHandler = new MetadataToolHandler(knowledgeGraphManager); 29 | this.dynamicHandler = new DynamicToolHandler(knowledgeGraphManager); 30 | 31 | this.initialized = true; 32 | } 33 | 34 | /** 35 | * Gets the appropriate handler for a given tool name 36 | */ 37 | static getHandler(toolName: string): BaseToolHandler { 38 | if (!this.initialized) { 39 | throw new Error('ToolHandlerFactory not initialized'); 40 | } 41 | 42 | // First check static tools 43 | if (toolName.match(/^(add|update|delete)_(nodes|edges)$/)) { 44 | return this.graphHandler; 45 | } 46 | if (toolName.match(/^(read_graph|search_nodes|open_nodes)$/)) { 47 | return this.searchHandler; 48 | } 49 | if (toolName.match(/^(add|delete)_metadata$/)) { 50 | return this.metadataHandler; 51 | } 52 | 53 | // Then check dynamic tools 54 | if (toolsRegistry.hasTool(toolName) && toolName.match(/^(add|update|delete)_/)) { 55 | return this.dynamicHandler; 56 | } 57 | 58 | throw new Error(`No handler found for tool: ${toolName}`); 59 | } 60 | 61 | /** 62 | * Checks if factory is initialized 63 | */ 64 | static isInitialized(): boolean { 65 | return this.initialized; 66 | } 67 | } -------------------------------------------------------------------------------- /src/integration/tools/handlers/index.ts: -------------------------------------------------------------------------------- 1 | // src/integration/tools/handlers/index.ts 2 | 3 | export {BaseToolHandler} from './BaseToolHandler.js'; 4 | export {GraphToolHandler} from './GraphToolHandler.js'; 5 | export {SearchToolHandler} from './SearchToolHandler.js'; 6 | export {MetadataToolHandler} from './MetadataToolHandler.js'; 7 | export {DynamicToolHandler} from './DynamicToolHandler.js'; 8 | export {ToolHandlerFactory} from './ToolHandlerFactory.js'; -------------------------------------------------------------------------------- /src/integration/tools/index.ts: -------------------------------------------------------------------------------- 1 | // src/integration/tools/index.ts 2 | 3 | export * from './registry/index.js'; 4 | export * from './handlers/index.js'; 5 | export { handleCallToolRequest } from './callToolHandler.js'; -------------------------------------------------------------------------------- /src/integration/tools/registry/dynamicTools.ts: -------------------------------------------------------------------------------- 1 | // src/tools/registry/dynamicTools.ts 2 | 3 | import {formatToolError} from '@shared/index.js'; 4 | import {initializeDynamicTools, type IDynamicSchemaToolRegistry} from '@integration/tools/DynamicSchemaToolRegistry.js'; 5 | import type {Tool, ToolResponse} from '@shared/index.js'; 6 | import type {ApplicationManager} from '@application/index.js'; 7 | 8 | /** 9 | * Manages dynamically generated tools based on schemas 10 | */ 11 | export class DynamicToolManager { 12 | private static instance: DynamicToolManager; 13 | private registry: IDynamicSchemaToolRegistry | null = null; 14 | private initialized = false; 15 | 16 | private constructor() { 17 | } 18 | 19 | /** 20 | * Gets the singleton instance of DynamicToolManager 21 | */ 22 | static getInstance(): DynamicToolManager { 23 | if (!DynamicToolManager.instance) { 24 | DynamicToolManager.instance = new DynamicToolManager(); 25 | } 26 | return DynamicToolManager.instance; 27 | } 28 | 29 | /** 30 | * Initializes the dynamic tool registry 31 | */ 32 | async initialize(): Promise { 33 | if (this.initialized) { 34 | return; 35 | } 36 | 37 | try { 38 | this.registry = await initializeDynamicTools(); 39 | this.initialized = true; 40 | } catch (error) { 41 | console.error('Failed to initialize dynamic tools:', error); 42 | throw new Error('Dynamic tools initialization failed'); 43 | } 44 | } 45 | 46 | /** 47 | * Gets all dynamically generated tools 48 | */ 49 | getTools(): Tool[] { 50 | if (!this.initialized || !this.registry) { 51 | throw new Error('Dynamic tools not initialized'); 52 | } 53 | return this.registry.getTools(); 54 | } 55 | 56 | /** 57 | * Handles a call to a dynamic tool 58 | */ 59 | async handleToolCall( 60 | toolName: string, 61 | args: Record, 62 | knowledgeGraphManager: ApplicationManager 63 | ): Promise { 64 | if (!this.initialized || !this.registry) { 65 | return formatToolError({ 66 | operation: toolName, 67 | error: 'Dynamic tools not initialized', 68 | suggestions: ["Initialize dynamic tools before use"], 69 | recoverySteps: ["Reinitialize DynamicToolManager"] 70 | }); 71 | } 72 | 73 | try { 74 | return await this.registry.handleToolCall(toolName, args, knowledgeGraphManager); 75 | } catch (error) { 76 | return formatToolError({ 77 | operation: toolName, 78 | error: error instanceof Error ? error.message : 'Unknown error occurred', 79 | context: { 80 | toolName, 81 | argumentKeys: Object.keys(args) 82 | }, 83 | suggestions: [ 84 | "Verify tool name matches schema", 85 | "Check argument format" 86 | ], 87 | recoverySteps: [ 88 | "Review tool schema requirements", 89 | "Ensure tool is properly registered" 90 | ] 91 | }); 92 | } 93 | } 94 | 95 | /** 96 | * Checks if a tool name corresponds to a dynamic tool 97 | */ 98 | isDynamicTool(toolName: string): boolean { 99 | if (!this.initialized || !this.registry) { 100 | return false; 101 | } 102 | return this.getTools().some(tool => tool.name === toolName); 103 | } 104 | } 105 | 106 | /** 107 | * Singleton instance of the DynamicToolManager 108 | */ 109 | export const dynamicToolManager = DynamicToolManager.getInstance(); 110 | 111 | /** 112 | * Re-export types that might be needed by consumers 113 | */ 114 | export type { 115 | Tool, 116 | ToolResponse, 117 | IDynamicSchemaToolRegistry 118 | }; -------------------------------------------------------------------------------- /src/integration/tools/registry/index.ts: -------------------------------------------------------------------------------- 1 | // src/integration/tools/registry/index.ts 2 | 3 | export {toolsRegistry} from './toolsRegistry.js'; 4 | export {dynamicToolManager} from './dynamicTools.js'; 5 | export {allStaticTools, graphTools, searchTools, metadataTools} from './staticTools.js'; -------------------------------------------------------------------------------- /src/integration/tools/registry/staticTools.ts: -------------------------------------------------------------------------------- 1 | // src/tools/registry/staticTools.ts 2 | 3 | import type {Tool} from '@shared/index.js'; 4 | 5 | /** 6 | * Graph manipulation tools for managing nodes and edges 7 | */ 8 | export const graphTools: Tool[] = [ 9 | { 10 | name: "add_nodes", 11 | description: "Add multiple new nodes in the knowledge graph", 12 | inputSchema: { 13 | type: "object", 14 | properties: { 15 | nodes: { 16 | type: "array", 17 | description: "Array of nodes to add", 18 | items: { 19 | type: "object", 20 | description: "Node to add", 21 | properties: { 22 | name: {type: "string", description: "The name of the node"}, 23 | nodeType: {type: "string", description: "The type of the node"}, 24 | metadata: { 25 | type: "array", 26 | items: {type: "string", description: "Metadata item"}, 27 | description: "An array of metadata contents associated with the node" 28 | }, 29 | }, 30 | required: ["name", "nodeType", "metadata"], 31 | }, 32 | }, 33 | }, 34 | required: ["nodes"], 35 | }, 36 | }, 37 | 38 | { 39 | name: "update_nodes", 40 | description: "Update existing nodes in the knowledge graph", 41 | inputSchema: { 42 | type: "object", 43 | properties: { 44 | nodes: { 45 | type: "array", 46 | description: "Array of nodes to update", 47 | items: { 48 | type: "object", 49 | description: "Node to update", 50 | properties: { 51 | name: {type: "string", description: "The name of the node to update"}, 52 | nodeType: {type: "string", description: "The new type of the node"}, 53 | metadata: { 54 | type: "array", 55 | items: {type: "string", description: "Metadata item"}, 56 | description: "An array of new metadata contents for the node" 57 | }, 58 | }, 59 | required: ["name"], 60 | }, 61 | }, 62 | }, 63 | required: ["nodes"], 64 | }, 65 | }, 66 | 67 | { 68 | name: "add_edges", 69 | description: "Add multiple new edges between nodes in the knowledge graph. Edges should be in active voice", 70 | inputSchema: { 71 | type: "object", 72 | properties: { 73 | edges: { 74 | type: "array", 75 | description: "Array of edges to add", 76 | items: { 77 | type: "object", 78 | description: "Edge to add", 79 | properties: { 80 | from: {type: "string", description: "The name of the node where the edge starts"}, 81 | to: {type: "string", description: "The name of the node where the edge ends"}, 82 | edgeType: {type: "string", description: "The type of the edge"}, 83 | weight: { 84 | type: "number", 85 | description: "Optional edge weight (0-1 range). Defaults to 1 if not specified", 86 | minimum: 0, 87 | maximum: 1 88 | } 89 | }, 90 | required: ["from", "to", "edgeType"], 91 | }, 92 | }, 93 | }, 94 | required: ["edges"], 95 | }, 96 | }, 97 | 98 | { 99 | name: "update_edges", 100 | description: "Update existing edges in the knowledge graph", 101 | inputSchema: { 102 | type: "object", 103 | properties: { 104 | edges: { 105 | type: "array", 106 | description: "Array of edges to update", 107 | items: { 108 | type: "object", 109 | description: "Edge to update", 110 | properties: { 111 | from: {type: "string", description: "Current source node name"}, 112 | to: {type: "string", description: "Current target node name"}, 113 | edgeType: {type: "string", description: "Current edge type"}, 114 | newFrom: {type: "string", description: "New source node name"}, 115 | newTo: {type: "string", description: "New target node name"}, 116 | newEdgeType: {type: "string", description: "New edge type"}, 117 | newWeight: { 118 | type: "number", 119 | description: "Optional new edge weight (0-1 range)", 120 | minimum: 0, 121 | maximum: 1 122 | } 123 | }, 124 | required: ["from", "to", "edgeType"], 125 | }, 126 | }, 127 | }, 128 | required: ["edges"], 129 | }, 130 | }, 131 | 132 | { 133 | name: "delete_nodes", 134 | description: "Delete multiple nodes and their associated edges from the knowledge graph", 135 | inputSchema: { 136 | type: "object", 137 | properties: { 138 | nodeNames: { 139 | type: "array", 140 | items: {type: "string", description: "Node name to delete"}, 141 | description: "An array of node names to delete" 142 | }, 143 | }, 144 | required: ["nodeNames"], 145 | }, 146 | }, 147 | 148 | { 149 | name: "delete_edges", 150 | description: "Delete multiple edges from the knowledge graph", 151 | inputSchema: { 152 | type: "object", 153 | properties: { 154 | edges: { 155 | type: "array", 156 | description: "Array of edges to delete", 157 | items: { 158 | type: "object", 159 | description: "Edge to delete", 160 | properties: { 161 | from: {type: "string", description: "The name of the node where the edge starts"}, 162 | to: {type: "string", description: "The name of the node where the edge ends"}, 163 | edgeType: {type: "string", description: "The type of the edge"}, 164 | }, 165 | required: ["from", "to", "edgeType"], 166 | }, 167 | }, 168 | }, 169 | required: ["edges"], 170 | }, 171 | }, 172 | ]; 173 | 174 | /** 175 | * Search-related tools for querying the knowledge graph 176 | */ 177 | export const searchTools: Tool[] = [ 178 | { 179 | name: "read_graph", 180 | description: "Read the entire knowledge graph", 181 | inputSchema: { 182 | type: "object", 183 | properties: {}, 184 | }, 185 | }, 186 | 187 | { 188 | name: "search_nodes", 189 | description: "Search for nodes in the knowledge graph based on a query", 190 | inputSchema: { 191 | type: "object", 192 | properties: { 193 | query: { 194 | type: "string", 195 | description: "The search query to match against node names, types, and metadata content" 196 | }, 197 | }, 198 | required: ["query"], 199 | }, 200 | }, 201 | 202 | { 203 | name: "open_nodes", 204 | description: "Open specific nodes in the knowledge graph by their names", 205 | inputSchema: { 206 | type: "object", 207 | properties: { 208 | names: { 209 | type: "array", 210 | items: {type: "string", description: "Node name to open"}, 211 | description: "An array of node names to retrieve", 212 | }, 213 | }, 214 | required: ["names"], 215 | }, 216 | } 217 | ]; 218 | 219 | /** 220 | * Metadata-related tools for managing node metadata 221 | */ 222 | export const metadataTools: Tool[] = [ 223 | { 224 | name: "add_metadata", 225 | description: "Add new metadata to existing nodes in the knowledge graph", 226 | inputSchema: { 227 | type: "object", 228 | properties: { 229 | metadata: { 230 | type: "array", 231 | description: "Array of metadata to add", 232 | items: { 233 | type: "object", 234 | description: "Metadata to add", 235 | properties: { 236 | nodeName: {type: "string", description: "The name of the node to add the metadata to"}, 237 | contents: { 238 | type: "array", 239 | items: {type: "string", description: "Metadata content item"}, 240 | description: "An array of metadata contents to add" 241 | }, 242 | }, 243 | required: ["nodeName", "contents"], 244 | }, 245 | }, 246 | }, 247 | required: ["metadata"], 248 | }, 249 | }, 250 | 251 | { 252 | name: "delete_metadata", 253 | description: "Delete specific metadata from nodes in the knowledge graph", 254 | inputSchema: { 255 | type: "object", 256 | properties: { 257 | deletions: { 258 | type: "array", 259 | description: "Array of metadata deletions", 260 | items: { 261 | type: "object", 262 | description: "Metadata deletion", 263 | properties: { 264 | nodeName: {type: "string", description: "The name of the node containing the metadata"}, 265 | metadata: { 266 | type: "array", 267 | items: {type: "string", description: "Metadata item to delete"}, 268 | description: "An array of metadata to delete" 269 | }, 270 | }, 271 | required: ["nodeName", "metadata"], 272 | }, 273 | }, 274 | }, 275 | required: ["deletions"], 276 | }, 277 | } 278 | ]; 279 | 280 | /** 281 | * Combined array of all static tools 282 | */ 283 | export const allStaticTools: Tool[] = [ 284 | ...graphTools, 285 | ...searchTools, 286 | ...metadataTools 287 | ]; -------------------------------------------------------------------------------- /src/integration/tools/registry/toolsRegistry.ts: -------------------------------------------------------------------------------- 1 | // src/tools/registry/toolsRegistry.ts 2 | 3 | import {allStaticTools} from './staticTools.js'; 4 | import {dynamicToolManager} from './dynamicTools.js'; 5 | import {formatToolError} from '@shared/index.js'; 6 | import type {Tool, ToolResponse} from '@shared/index.js'; 7 | import type {ApplicationManager} from '@application/index.js'; 8 | 9 | /** 10 | * Central registry for all tools (both static and dynamic) 11 | */ 12 | export class ToolsRegistry { 13 | private static instance: ToolsRegistry; 14 | private initialized = false; 15 | private tools: Map = new Map(); 16 | private knowledgeGraphManager: ApplicationManager | null = null; 17 | 18 | private constructor() { 19 | } 20 | 21 | /** 22 | * Gets the singleton instance of ToolsRegistry 23 | */ 24 | static getInstance(): ToolsRegistry { 25 | if (!ToolsRegistry.instance) { 26 | ToolsRegistry.instance = new ToolsRegistry(); 27 | } 28 | return ToolsRegistry.instance; 29 | } 30 | 31 | /** 32 | * Initializes the registry with both static and dynamic tools 33 | */ 34 | async initialize(knowledgeGraphManager: ApplicationManager): Promise { 35 | if (this.initialized) { 36 | return; 37 | } 38 | 39 | try { 40 | this.knowledgeGraphManager = knowledgeGraphManager; 41 | 42 | // Register static tools 43 | allStaticTools.forEach(tool => { 44 | this.tools.set(tool.name, tool); 45 | }); 46 | 47 | // Initialize and register dynamic tools 48 | await dynamicToolManager.initialize(); 49 | dynamicToolManager.getTools().forEach(tool => { 50 | this.tools.set(tool.name, tool); 51 | }); 52 | 53 | this.initialized = true; 54 | console.error(`[ToolsRegistry] Initialized with ${this.tools.size} tools`); 55 | } catch (error) { 56 | console.error('[ToolsRegistry] Initialization error:', error); 57 | throw error; 58 | } 59 | } 60 | 61 | /** 62 | * Gets a specific tool by name 63 | */ 64 | getTool(name: string): Tool | undefined { 65 | this.ensureInitialized(); 66 | return this.tools.get(name); 67 | } 68 | 69 | /** 70 | * Gets all registered tools 71 | */ 72 | getAllTools(): Tool[] { 73 | this.ensureInitialized(); 74 | return Array.from(this.tools.values()); 75 | } 76 | 77 | /** 78 | * Handles a tool call by delegating to the appropriate handler 79 | */ 80 | async handleToolCall(toolName: string, args: Record): Promise { 81 | this.ensureInitialized(); 82 | 83 | if (!this.knowledgeGraphManager) { 84 | return formatToolError({ 85 | operation: toolName, 86 | error: 'KnowledgeGraphManager not initialized', 87 | suggestions: ["Initialize registry with KnowledgeGraphManager"], 88 | recoverySteps: ["Reinitialize ToolsRegistry with proper dependencies"] 89 | }); 90 | } 91 | 92 | try { 93 | if (!this.tools.has(toolName)) { 94 | return formatToolError({ 95 | operation: toolName, 96 | error: `Tool not found: ${toolName}`, 97 | context: {availableTools: Array.from(this.tools.keys())}, 98 | suggestions: ["Verify tool name exists"] 99 | }); 100 | } 101 | 102 | if (dynamicToolManager.isDynamicTool(toolName)) { 103 | return await dynamicToolManager.handleToolCall( 104 | toolName, 105 | args, 106 | this.knowledgeGraphManager 107 | ); 108 | } 109 | 110 | // For static tools, return success response 111 | return { 112 | toolResult: { 113 | isError: false, 114 | data: args, 115 | actionTaken: `Executed tool: ${toolName}`, 116 | timestamp: new Date().toISOString(), 117 | content: [] 118 | } 119 | }; 120 | } catch (error) { 121 | return formatToolError({ 122 | operation: toolName, 123 | error: error instanceof Error ? error.message : 'Unknown error occurred', 124 | context: {toolName, args}, 125 | suggestions: [ 126 | "Check tool name and arguments", 127 | "Verify tool registration" 128 | ], 129 | recoverySteps: [ 130 | "Review tool documentation", 131 | "Ensure tool is properly registered" 132 | ] 133 | }); 134 | } 135 | } 136 | 137 | /** 138 | * Checks if a specific tool exists 139 | */ 140 | hasTool(name: string): boolean { 141 | this.ensureInitialized(); 142 | return this.tools.has(name); 143 | } 144 | 145 | /** 146 | * Ensures the registry is initialized before use 147 | */ 148 | private ensureInitialized(): void { 149 | if (!this.initialized) { 150 | throw new Error('Tools registry not initialized'); 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * Singleton instance of the ToolsRegistry 157 | */ 158 | export const toolsRegistry = ToolsRegistry.getInstance(); 159 | 160 | /** 161 | * Re-export types that might be needed by consumers 162 | */ 163 | export type { 164 | Tool, 165 | ToolResponse, 166 | ApplicationManager 167 | }; -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | // src/shared/index.ts 2 | 3 | export * from './types/index.js'; 4 | export * from './utils/index.js'; -------------------------------------------------------------------------------- /src/shared/types/index.ts: -------------------------------------------------------------------------------- 1 | // src/shared/types/index.ts 2 | 3 | export * from './operations.js'; 4 | export * from './tools.js'; -------------------------------------------------------------------------------- /src/shared/types/operations.ts: -------------------------------------------------------------------------------- 1 | // src/types/operations.ts 2 | 3 | import type {Node, Edge, Graph} from '@core/index.js'; 4 | 5 | /** 6 | * Edge update operation parameters 7 | */ 8 | export interface EdgeUpdate { 9 | from: string; 10 | to: string; 11 | edgeType: string; 12 | newFrom?: string; 13 | newTo?: string; 14 | newEdgeType?: string; 15 | newWeight?: number; 16 | } 17 | 18 | /** 19 | * Edge filter parameters 20 | */ 21 | export interface EdgeFilter { 22 | from?: string; 23 | to?: string; 24 | edgeType?: string; 25 | } 26 | 27 | /** 28 | * Metadata addition parameters 29 | */ 30 | export interface MetadataAddition { 31 | nodeName: string; 32 | contents: string[]; 33 | } 34 | 35 | /** 36 | * Metadata deletion parameters 37 | */ 38 | export interface MetadataDeletion { 39 | nodeName: string; 40 | metadata: string[]; 41 | } 42 | 43 | /** 44 | * Result of metadata operation 45 | */ 46 | export interface MetadataResult { 47 | nodeName: string; 48 | addedMetadata: string[]; 49 | } 50 | 51 | /** 52 | * Result of getting edges 53 | */ 54 | export interface GetEdgesResult { 55 | edges: Edge[]; 56 | } 57 | 58 | /** 59 | * Result of opening or searching nodes 60 | */ 61 | export interface OpenNodesResult { 62 | nodes: Node[]; 63 | edges: Edge[]; 64 | } 65 | 66 | // Operation Results 67 | export interface GraphOperationResult { 68 | success: boolean; 69 | nodes?: Node[]; 70 | edges?: Edge[]; 71 | error?: string; 72 | } 73 | 74 | export interface SearchOperationResult { 75 | success: boolean; 76 | result?: Graph; 77 | error?: string; 78 | } 79 | 80 | export interface MetadataOperationResult { 81 | success: boolean; 82 | results?: MetadataResult[]; 83 | error?: string; 84 | } -------------------------------------------------------------------------------- /src/shared/types/tools.ts: -------------------------------------------------------------------------------- 1 | // src/types/tools.ts 2 | 3 | /** 4 | * Schema definition for tool parameters 5 | */ 6 | export interface ToolSchema { 7 | type: string; 8 | properties: Record; 9 | required?: string[]; 10 | } 11 | 12 | /** 13 | * Property definition in a tool schema 14 | */ 15 | export interface SchemaProperty { 16 | type: string; 17 | description: string; 18 | items?: SchemaProperty; 19 | properties?: Record; 20 | required?: string[]; 21 | enum?: string[]; 22 | minimum?: number; 23 | maximum?: number; 24 | } 25 | 26 | /** 27 | * Tool definition 28 | */ 29 | export interface Tool { 30 | name: string; 31 | description: string; 32 | inputSchema: ToolSchema; 33 | } 34 | 35 | /** 36 | * Content item in a tool result 37 | */ 38 | export interface ToolResultContent { 39 | type: string; 40 | text: string; 41 | } 42 | 43 | /** 44 | * Core result template that both success and error responses use 45 | */ 46 | export interface ToolResult { 47 | isError: boolean; 48 | content: ToolResultContent[]; // For message display 49 | data?: T; 50 | actionTaken?: string; 51 | timestamp: string; 52 | suggestions?: string[]; 53 | recoverySteps?: string[]; 54 | } 55 | 56 | /** 57 | * Wrapper interface that matches the MCP server response format 58 | */ 59 | export interface ToolResponse { 60 | toolResult: ToolResult; 61 | } 62 | 63 | /** 64 | * Options for formatting a tool response 65 | */ 66 | export interface ToolResponseOptions { 67 | data?: T; 68 | message?: string; 69 | actionTaken?: string; 70 | suggestions?: string[]; 71 | } 72 | 73 | /** 74 | * Options for formatting a tool error 75 | */ 76 | export interface ToolErrorOptions { 77 | operation: string; 78 | error: string; 79 | context?: Record; 80 | suggestions?: string[]; 81 | recoverySteps?: string[]; 82 | } 83 | 84 | /** 85 | * Options for formatting a partial success response 86 | */ 87 | export interface PartialSuccessOptions { 88 | operation: string; 89 | attempted: T[]; 90 | succeeded: T[]; 91 | failed: T[]; 92 | details: Record; 93 | } 94 | 95 | /** 96 | * Request format for tool calls 97 | */ 98 | export interface ToolCallRequest { 99 | params: { 100 | name: string; 101 | arguments?: Record; 102 | }; 103 | } -------------------------------------------------------------------------------- /src/shared/utils/index.ts: -------------------------------------------------------------------------------- 1 | // src/shared/utils/index.ts 2 | 3 | export { 4 | formatToolResponse, 5 | formatToolError, 6 | formatPartialSuccess 7 | } from './responseFormatter.js'; -------------------------------------------------------------------------------- /src/shared/utils/responseFormatter.ts: -------------------------------------------------------------------------------- 1 | // src/utils/responseFormatter.ts 2 | 3 | import type { 4 | ToolResponse, 5 | ToolResult, 6 | ToolResponseOptions, 7 | ToolErrorOptions, 8 | PartialSuccessOptions 9 | } from '@shared/index.js'; 10 | 11 | /** 12 | * Formats successful tool responses in a consistent, AI-friendly way. 13 | */ 14 | export function formatToolResponse({ 15 | data, 16 | message, 17 | actionTaken, 18 | suggestions = [] 19 | }: ToolResponseOptions): ToolResponse { 20 | const toolResult: ToolResult = { 21 | isError: false, 22 | content: message ? [{type: "text", text: message}] : [], 23 | timestamp: new Date().toISOString() 24 | }; 25 | 26 | if (data !== undefined) { 27 | toolResult.data = data; 28 | } 29 | 30 | if (actionTaken) { 31 | toolResult.actionTaken = actionTaken; 32 | } 33 | 34 | if (suggestions.length > 0) { 35 | toolResult.suggestions = suggestions; 36 | } 37 | 38 | return {toolResult}; 39 | } 40 | 41 | /** 42 | * Formats error responses in a consistent, AI-friendly way. 43 | */ 44 | export function formatToolError({ 45 | operation, 46 | error, 47 | context, 48 | suggestions = [], 49 | recoverySteps = [] 50 | }: ToolErrorOptions): ToolResponse { 51 | const toolResult: ToolResult = { 52 | isError: true, 53 | content: [ 54 | {type: "text", text: `Error during ${operation}: ${error}`}, 55 | ...(context ? [{type: "text", text: `Context: ${JSON.stringify(context)}`}] : []) 56 | ], 57 | timestamp: new Date().toISOString() 58 | }; 59 | 60 | if (suggestions.length > 0) { 61 | toolResult.suggestions = suggestions; 62 | } 63 | 64 | if (recoverySteps.length > 0) { 65 | toolResult.recoverySteps = recoverySteps; 66 | } 67 | 68 | return {toolResult}; 69 | } 70 | 71 | /** 72 | * Creates an informative message for partial success scenarios. 73 | */ 74 | export function formatPartialSuccess({ 75 | operation, 76 | attempted, 77 | succeeded, 78 | failed, 79 | details 80 | }: PartialSuccessOptions): ToolResponse { 81 | const toolResult: ToolResult = { 82 | isError: true, 83 | content: [ 84 | { 85 | type: "text", 86 | text: `Partial success for ${operation}: ${succeeded.length} succeeded, ${failed.length} failed` 87 | }, 88 | { 89 | type: "text", 90 | text: `Details: ${JSON.stringify(details)}` 91 | } 92 | ], 93 | data: { 94 | succeededItems: succeeded, 95 | failedItems: failed.map(item => ({ 96 | item, 97 | reason: details[String(item)] || 'Unknown error' 98 | })) 99 | }, 100 | suggestions: [ 101 | "Review failed items and their reasons", 102 | "Consider retrying failed operations individually", 103 | "Verify requirements for failed items" 104 | ], 105 | timestamp: new Date().toISOString() 106 | }; 107 | 108 | return {toolResult}; 109 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true, 11 | "strict": true, 12 | "sourceMap": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "allowJs": true, 15 | "checkJs": false, 16 | "declaration": true, 17 | "skipLibCheck": true, 18 | "baseUrl": "src", 19 | "paths": { 20 | "@core/*": [ 21 | "core/*" 22 | ], 23 | "@infrastructure/*": [ 24 | "infrastructure/*" 25 | ], 26 | "@application/*": [ 27 | "application/*" 28 | ], 29 | "@integration/*": [ 30 | "integration/*" 31 | ], 32 | "@shared/*": [ 33 | "shared/*" 34 | ], 35 | "@data/*": [ 36 | "data/*" 37 | ], 38 | "@config/*": [ 39 | "config/*" 40 | ] 41 | } 42 | }, 43 | "include": [ 44 | "src/**/*" 45 | ], 46 | "exclude": [ 47 | "node_modules", 48 | "dist" 49 | ] 50 | } --------------------------------------------------------------------------------