#
tokens: 30546/50000 1/128 files (page 9/13)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 9 of 13. Use http://codebase.md/tejpalvirk/contextmanager?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .gitignore
├── build-all-domains.sh
├── developer
│   ├── .gitattributes
│   ├── developer_advancedcontext.txt
│   ├── developer_buildcontext.txt
│   ├── developer_deletecontext.txt
│   ├── developer_endsession_examples.txt
│   ├── developer_endsession.txt
│   ├── developer_loadcontext.txt
│   ├── developer_startsession.txt
│   ├── Dockerfile
│   ├── index.d.ts
│   ├── index.js
│   ├── index.ts
│   ├── package.json
│   ├── README.md
│   └── tsconfig.json
├── dist
│   ├── developer
│   │   ├── index.d.ts
│   │   └── index.js
│   ├── main
│   │   ├── descriptions
│   │   │   ├── common_advancedcontext.txt
│   │   │   ├── common_buildcontext.txt
│   │   │   ├── common_deletecontext.txt
│   │   │   ├── common_endsession.txt
│   │   │   ├── common_loadcontext.txt
│   │   │   ├── common_startsession.txt
│   │   │   ├── developer_advancedcontext.txt
│   │   │   ├── developer_buildcontext.txt
│   │   │   ├── developer_deletecontext.txt
│   │   │   ├── developer_endsession_examples.txt
│   │   │   ├── developer_endsession.txt
│   │   │   ├── developer_loadcontext.txt
│   │   │   ├── developer_startsession.txt
│   │   │   ├── project_advancedcontext.txt
│   │   │   ├── project_buildcontext.txt
│   │   │   ├── project_deletecontext.txt
│   │   │   ├── project_endsession_examples.txt
│   │   │   ├── project_endsession.txt
│   │   │   ├── project_loadcontext.txt
│   │   │   ├── project_startsession.txt
│   │   │   ├── qualitativeresearch_advancedcontext.txt
│   │   │   ├── qualitativeresearch_buildcontext.txt
│   │   │   ├── qualitativeresearch_deletecontext.txt
│   │   │   ├── qualitativeresearch_endsession_examples.txt
│   │   │   ├── qualitativeresearch_endsession.txt
│   │   │   ├── qualitativeresearch_loadcontext.txt
│   │   │   ├── qualitativeresearch_startsession.txt
│   │   │   ├── quantitativeresearch_advancedcontext.txt
│   │   │   ├── quantitativeresearch_buildcontext.txt
│   │   │   ├── quantitativeresearch_deletecontext.txt
│   │   │   ├── quantitativeresearch_endsession_examples.txt
│   │   │   ├── quantitativeresearch_endsession.txt
│   │   │   ├── quantitativeresearch_loadcontext.txt
│   │   │   ├── quantitativeresearch_startsession.txt
│   │   │   ├── student_advancedcontext.txt
│   │   │   ├── student_buildcontext.txt
│   │   │   ├── student_deletecontext.txt
│   │   │   ├── student_endsession_examples.txt
│   │   │   ├── student_endsession.txt
│   │   │   ├── student_loadcontext.txt
│   │   │   └── student_startsession.txt
│   │   ├── index.d.ts
│   │   ├── index.js
│   │   ├── mcp.d.ts
│   │   └── mcp.js
│   ├── project
│   │   ├── index.d.ts
│   │   └── index.js
│   ├── qualitativeresearch
│   │   ├── index.d.ts
│   │   └── index.js
│   ├── quantitativeresearch
│   │   ├── index.d.ts
│   │   └── index.js
│   └── student
│       ├── index.d.ts
│       └── index.js
├── main
│   ├── descriptions
│   │   ├── common_advancedcontext.txt
│   │   ├── common_buildcontext.txt
│   │   ├── common_deletecontext.txt
│   │   ├── common_endsession.txt
│   │   ├── common_loadcontext.txt
│   │   ├── common_startsession.txt
│   │   ├── developer_advancedcontext.txt
│   │   ├── developer_buildcontext.txt
│   │   ├── developer_deletecontext.txt
│   │   ├── developer_endsession_examples.txt
│   │   ├── developer_endsession.txt
│   │   ├── developer_loadcontext.txt
│   │   ├── developer_startsession.txt
│   │   ├── project_advancedcontext.txt
│   │   ├── project_buildcontext.txt
│   │   ├── project_deletecontext.txt
│   │   ├── project_endsession_examples.txt
│   │   ├── project_endsession.txt
│   │   ├── project_loadcontext.txt
│   │   ├── project_startsession.txt
│   │   ├── qualitativeresearch_advancedcontext.txt
│   │   ├── qualitativeresearch_buildcontext.txt
│   │   ├── qualitativeresearch_deletecontext.txt
│   │   ├── qualitativeresearch_endsession_examples.txt
│   │   ├── qualitativeresearch_endsession.txt
│   │   ├── qualitativeresearch_loadcontext.txt
│   │   ├── qualitativeresearch_startsession.txt
│   │   ├── quantitativeresearch_advancedcontext.txt
│   │   ├── quantitativeresearch_buildcontext.txt
│   │   ├── quantitativeresearch_deletecontext.txt
│   │   ├── quantitativeresearch_endsession_examples.txt
│   │   ├── quantitativeresearch_endsession.txt
│   │   ├── quantitativeresearch_loadcontext.txt
│   │   ├── quantitativeresearch_startsession.txt
│   │   ├── student_advancedcontext.txt
│   │   ├── student_buildcontext.txt
│   │   ├── student_deletecontext.txt
│   │   ├── student_endsession_examples.txt
│   │   ├── student_endsession.txt
│   │   ├── student_loadcontext.txt
│   │   └── student_startsession.txt
│   ├── index.js
│   ├── index.ts
│   ├── mcp.ts
│   ├── package.json
│   ├── README.md
│   └── tsconfig.json
├── package-lock.json
├── package.json
├── project
│   ├── .gitattributes
│   ├── Dockerfile
│   ├── index.d.ts
│   ├── index.js
│   ├── index.ts
│   ├── package.json
│   ├── project_advancedcontext.txt
│   ├── project_buildcontext.txt
│   ├── project_deletecontext.txt
│   ├── project_endsession_examples.txt
│   ├── project_endsession.txt
│   ├── project_loadcontext.txt
│   ├── project_startsession.txt
│   ├── README.md
│   └── tsconfig.json
├── qualitativeresearch
│   ├── .gitattributes
│   ├── Dockerfile
│   ├── index.d.ts
│   ├── index.js
│   ├── index.ts
│   ├── package.json
│   ├── qualitativeresearch_advancedcontext.txt
│   ├── qualitativeresearch_buildcontext.txt
│   ├── qualitativeresearch_deletecontext.txt
│   ├── qualitativeresearch_endsession_examples.txt
│   ├── qualitativeresearch_endsession.txt
│   ├── qualitativeresearch_loadcontext.txt
│   ├── qualitativeresearch_startsession.txt
│   ├── README.md
│   └── tsconfig.json
├── quantitativeresearch
│   ├── .gitattributes
│   ├── Dockerfile
│   ├── index.d.ts
│   ├── index.js
│   ├── index.ts
│   ├── package.json
│   ├── quantitativeresearch_advancedcontext.txt
│   ├── quantitativeresearch_buildcontext.txt
│   ├── quantitativeresearch_deletecontext.txt
│   ├── quantitativeresearch_endsession_examples.txt
│   ├── quantitativeresearch_endsession.txt
│   ├── quantitativeresearch_loadcontext.txt
│   ├── quantitativeresearch_startsession.txt
│   ├── README.md
│   └── tsconfig.json
├── README.md
├── student
│   ├── .gitattributes
│   ├── Dockerfile
│   ├── index.d.ts
│   ├── index.js
│   ├── index.ts
│   ├── package.json
│   ├── README.md
│   ├── student_advancedcontext.txt
│   ├── student_buildcontext.txt
│   ├── student_deletecontext.txt
│   ├── student_endsession_examples.txt
│   ├── student_endsession.txt
│   ├── student_loadcontext.txt
│   ├── student_startsession.txt
│   └── tsconfig.json
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/qualitativeresearch/index.js:
--------------------------------------------------------------------------------

```javascript
   1 | #!/usr/bin/env node
   2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
   3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
   4 | import { promises as fs } from 'fs';
   5 | import path from 'path';
   6 | import { fileURLToPath } from 'url';
   7 | import { z } from "zod";
   8 | import { readFileSync, existsSync } from "fs";
   9 | // Define memory file path using environment variable with fallback
  10 | const parentPath = path.dirname(fileURLToPath(import.meta.url));
  11 | const defaultMemoryPath = path.join(parentPath, 'memory.json');
  12 | const defaultSessionsPath = path.join(parentPath, 'sessions.json');
  13 | // Properly handle absolute and relative paths for MEMORY_FILE_PATH
  14 | const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH
  15 |     ? path.isAbsolute(process.env.MEMORY_FILE_PATH)
  16 |         ? process.env.MEMORY_FILE_PATH // Use absolute path as is
  17 |         : path.join(process.cwd(), process.env.MEMORY_FILE_PATH) // Relative to current working directory
  18 |     : defaultMemoryPath; // Default fallback
  19 | // Properly handle absolute and relative paths for SESSIONS_FILE_PATH
  20 | const SESSIONS_FILE_PATH = process.env.SESSIONS_FILE_PATH
  21 |     ? path.isAbsolute(process.env.SESSIONS_FILE_PATH)
  22 |         ? process.env.SESSIONS_FILE_PATH // Use absolute path as is
  23 |         : path.join(process.cwd(), process.env.SESSIONS_FILE_PATH) // Relative to current working directory
  24 |     : defaultSessionsPath; // Default fallback
  25 | // Qualitative Research specific entity types
  26 | const VALID_ENTITY_TYPES = [
  27 |     'project', // Overall research study
  28 |     'participant', // Research subjects
  29 |     'interview', // Formal conversation with participants
  30 |     'observation', // Field notes from observational research
  31 |     'document', // External materials being analyzed
  32 |     'code', // Labels applied to data segments
  33 |     'codeGroup', // Categories or families of related codes
  34 |     'memo', // Researcher's analytical notes
  35 |     'theme', // Emergent patterns across data
  36 |     'quote', // Notable excerpts from data sources
  37 |     'literature', // Academic sources
  38 |     'researchQuestion', // Formal questions guiding the study
  39 |     'finding', // Results or conclusions
  40 |     'status', // Status entity type
  41 |     'priority' // Priority entity type
  42 | ];
  43 | // Qualitative Research specific relation types
  44 | const VALID_RELATION_TYPES = [
  45 |     'participated_in', // Links participants to interviews/observations
  46 |     'codes', // Shows which codes apply to which data
  47 |     'contains', // Hierarchical relationship (e.g., codegroup contains codes)
  48 |     'supports', // Data supporting a theme or finding
  49 |     'contradicts', // Data contradicting a theme or finding
  50 |     'answers', // Data addressing a research question
  51 |     'cites', // References to literature
  52 |     'followed_by', // Temporal sequence
  53 |     'related_to', // General connection
  54 |     'reflects_on', // Memo reflecting on data/code/theme
  55 |     'compares', // Comparative relationship
  56 |     'conducted_by', // Person who conducted data collection
  57 |     'transcribed_by', // Person who transcribed data
  58 |     'part_of', // Entity is part of another entity
  59 |     'derived_from', // Entity is derived from another entity
  60 |     'collected_on', // Data collection date
  61 |     'analyzes', // Analysis relationship
  62 |     'triangulates_with', // Triangulation between data sources
  63 |     'has_status', // Entity has a specific status
  64 |     'has_priority', // Entity has a specific priority
  65 |     'precedes' // Entity comes before another entity in sequence
  66 | ];
  67 | // Status values for different entity types in qualitative research
  68 | const STATUS_VALUES = {
  69 |     project: ['planning', 'data_collection', 'analysis', 'writing', 'complete'],
  70 |     interview: ['scheduled', 'conducted', 'transcribed', 'coded', 'analyzed'],
  71 |     observation: ['planned', 'conducted', 'documented', 'coded', 'analyzed'],
  72 |     code: ['initial', 'revised', 'final'],
  73 |     theme: ['emerging', 'developing', 'established'],
  74 |     finding: ['preliminary', 'draft', 'final']
  75 | };
  76 | // Define valid status values for all entity types
  77 | const VALID_STATUS_VALUES = [
  78 |     'planning', 'data_collection', 'analysis', 'writing', 'complete',
  79 |     'scheduled', 'conducted', 'transcribed', 'coded', 'analyzed',
  80 |     'planned', 'documented',
  81 |     'initial', 'revised', 'final',
  82 |     'emerging', 'developing', 'established',
  83 |     'preliminary', 'draft',
  84 |     'active', 'in_progress', 'not_started'
  85 | ];
  86 | // Define valid priority values
  87 | const VALID_PRIORITY_VALUES = [
  88 |     'high', 'low'
  89 | ];
  90 | // Basic validation functions
  91 | function validateEntityType(entityType) {
  92 |     return VALID_ENTITY_TYPES.includes(entityType);
  93 | }
  94 | function validateRelationType(relationType) {
  95 |     return VALID_RELATION_TYPES.includes(relationType);
  96 | }
  97 | const __filename = fileURLToPath(import.meta.url);
  98 | const __dirname = path.dirname(__filename);
  99 | // Collect tool descriptions from text files
 100 | const toolDescriptions = {
 101 |     'startsession': '',
 102 |     'loadcontext': '',
 103 |     'deletecontext': '',
 104 |     'buildcontext': '',
 105 |     'advancedcontext': '',
 106 |     'endsession': '',
 107 | };
 108 | for (const tool of Object.keys(toolDescriptions)) {
 109 |     const descriptionFilePath = path.resolve(__dirname, `qualitativeresearch_${tool}.txt`);
 110 |     if (existsSync(descriptionFilePath)) {
 111 |         toolDescriptions[tool] = readFileSync(descriptionFilePath, 'utf-8');
 112 |     }
 113 | }
 114 | // Session management functions
 115 | async function loadSessionStates() {
 116 |     try {
 117 |         const fileContent = await fs.readFile(SESSIONS_FILE_PATH, 'utf-8');
 118 |         const sessions = JSON.parse(fileContent);
 119 |         // Convert from object to Map
 120 |         const sessionsMap = new Map();
 121 |         for (const [key, value] of Object.entries(sessions)) {
 122 |             sessionsMap.set(key, value);
 123 |         }
 124 |         return sessionsMap;
 125 |     }
 126 |     catch (error) {
 127 |         if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
 128 |             return new Map();
 129 |         }
 130 |         throw error;
 131 |     }
 132 | }
 133 | async function saveSessionStates(sessionsMap) {
 134 |     // Convert from Map to object
 135 |     const sessions = {};
 136 |     for (const [key, value] of sessionsMap.entries()) {
 137 |         sessions[key] = value;
 138 |     }
 139 |     await fs.writeFile(SESSIONS_FILE_PATH, JSON.stringify(sessions, null, 2), 'utf-8');
 140 | }
 141 | // Generate a unique session ID
 142 | function generateSessionId() {
 143 |     return `qual_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
 144 | }
 145 | // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
 146 | class KnowledgeGraphManager {
 147 |     async loadGraph() {
 148 |         try {
 149 |             const fileContent = await fs.readFile(MEMORY_FILE_PATH, 'utf-8');
 150 |             return JSON.parse(fileContent);
 151 |         }
 152 |         catch (error) {
 153 |             // If the file doesn't exist, return an empty graph
 154 |             return {
 155 |                 entities: [],
 156 |                 relations: []
 157 |             };
 158 |         }
 159 |     }
 160 |     async saveGraph(graph) {
 161 |         await fs.writeFile(MEMORY_FILE_PATH, JSON.stringify(graph, null, 2), 'utf-8');
 162 |     }
 163 |     // Initialize status and priority entities
 164 |     async initializeStatusAndPriority() {
 165 |         const graph = await this.loadGraph();
 166 |         // Create status entities if they don't exist
 167 |         for (const statusValue of VALID_STATUS_VALUES) {
 168 |             const statusName = `status:${statusValue}`;
 169 |             if (!graph.entities.some(e => e.name === statusName && e.entityType === 'status')) {
 170 |                 graph.entities.push({
 171 |                     name: statusName,
 172 |                     entityType: 'status',
 173 |                     observations: [`A ${statusValue} status value`]
 174 |                 });
 175 |             }
 176 |         }
 177 |         // Create priority entities if they don't exist
 178 |         for (const priorityValue of VALID_PRIORITY_VALUES) {
 179 |             const priorityName = `priority:${priorityValue}`;
 180 |             if (!graph.entities.some(e => e.name === priorityName && e.entityType === 'priority')) {
 181 |                 graph.entities.push({
 182 |                     name: priorityName,
 183 |                     entityType: 'priority',
 184 |                     observations: [`A ${priorityValue} priority value`]
 185 |                 });
 186 |             }
 187 |         }
 188 |         await this.saveGraph(graph);
 189 |     }
 190 |     // Helper method to get status of an entity
 191 |     async getEntityStatus(entityName) {
 192 |         const graph = await this.loadGraph();
 193 |         // Find status relation for this entity
 194 |         const statusRelation = graph.relations.find(r => r.from === entityName &&
 195 |             r.relationType === 'has_status');
 196 |         if (statusRelation) {
 197 |             // Extract status value from the status entity name (status:value)
 198 |             return statusRelation.to.split(':')[1];
 199 |         }
 200 |         return null;
 201 |     }
 202 |     // Helper method to get priority of an entity
 203 |     async getEntityPriority(entityName) {
 204 |         const graph = await this.loadGraph();
 205 |         // Find priority relation for this entity
 206 |         const priorityRelation = graph.relations.find(r => r.from === entityName &&
 207 |             r.relationType === 'has_priority');
 208 |         if (priorityRelation) {
 209 |             // Extract priority value from the priority entity name (priority:value)
 210 |             return priorityRelation.to.split(':')[1];
 211 |         }
 212 |         return null;
 213 |     }
 214 |     // Helper method to set status of an entity
 215 |     async setEntityStatus(entityName, statusValue) {
 216 |         if (!VALID_STATUS_VALUES.includes(statusValue)) {
 217 |             throw new Error(`Invalid status value: ${statusValue}. Valid values are: ${VALID_STATUS_VALUES.join(', ')}`);
 218 |         }
 219 |         const graph = await this.loadGraph();
 220 |         // Remove any existing status relations for this entity
 221 |         graph.relations = graph.relations.filter(r => !(r.from === entityName && r.relationType === 'has_status'));
 222 |         // Add new status relation
 223 |         graph.relations.push({
 224 |             from: entityName,
 225 |             to: `status:${statusValue}`,
 226 |             relationType: 'has_status'
 227 |         });
 228 |         await this.saveGraph(graph);
 229 |     }
 230 |     // Helper method to set priority of an entity
 231 |     async setEntityPriority(entityName, priorityValue) {
 232 |         if (!VALID_PRIORITY_VALUES.includes(priorityValue)) {
 233 |             throw new Error(`Invalid priority value: ${priorityValue}. Valid values are: ${VALID_PRIORITY_VALUES.join(', ')}`);
 234 |         }
 235 |         const graph = await this.loadGraph();
 236 |         // Remove any existing priority relations for this entity
 237 |         graph.relations = graph.relations.filter(r => !(r.from === entityName && r.relationType === 'has_priority'));
 238 |         // Add new priority relation
 239 |         graph.relations.push({
 240 |             from: entityName,
 241 |             to: `priority:${priorityValue}`,
 242 |             relationType: 'has_priority'
 243 |         });
 244 |         await this.saveGraph(graph);
 245 |     }
 246 |     async createEntities(entities) {
 247 |         const graph = await this.loadGraph();
 248 |         const existingEntityNames = new Set(graph.entities.map(e => e.name));
 249 |         // Validate entity types
 250 |         entities.forEach(entity => {
 251 |             if (!validateEntityType(entity.entityType)) {
 252 |                 throw new Error(`Invalid entity type: ${entity.entityType}. Valid types are: ${VALID_ENTITY_TYPES.join(', ')}`);
 253 |             }
 254 |         });
 255 |         const newEntities = entities.filter(entity => !existingEntityNames.has(entity.name));
 256 |         graph.entities.push(...newEntities);
 257 |         await this.saveGraph(graph);
 258 |         return newEntities;
 259 |     }
 260 |     async createRelations(relations) {
 261 |         const graph = await this.loadGraph();
 262 |         const existingEntityNames = new Set(graph.entities.map(e => e.name));
 263 |         // Check that entities exist and validate relation types
 264 |         relations.forEach(relation => {
 265 |             if (!existingEntityNames.has(relation.from)) {
 266 |                 throw new Error(`Entity '${relation.from}' not found`);
 267 |             }
 268 |             if (!existingEntityNames.has(relation.to)) {
 269 |                 throw new Error(`Entity '${relation.to}' not found`);
 270 |             }
 271 |             if (!validateRelationType(relation.relationType)) {
 272 |                 throw new Error(`Invalid relation type: ${relation.relationType}. Valid types are: ${VALID_RELATION_TYPES.join(', ')}`);
 273 |             }
 274 |         });
 275 |         // Filter out duplicate relations
 276 |         const existingRelations = new Set(graph.relations.map(r => `${r.from}:${r.to}:${r.relationType}`));
 277 |         const newRelations = relations.filter(r => !existingRelations.has(`${r.from}:${r.to}:${r.relationType}`));
 278 |         graph.relations.push(...newRelations);
 279 |         await this.saveGraph(graph);
 280 |         return newRelations;
 281 |     }
 282 |     async addObservations(observations) {
 283 |         const graph = await this.loadGraph();
 284 |         const results = [];
 285 |         for (const observation of observations) {
 286 |             const entity = graph.entities.find(e => e.name === observation.entityName);
 287 |             if (!entity) {
 288 |                 throw new Error(`Entity '${observation.entityName}' not found`);
 289 |             }
 290 |             // Filter out duplicate observations
 291 |             const existingObservations = new Set(entity.observations);
 292 |             const newObservations = observation.contents.filter(o => !existingObservations.has(o));
 293 |             entity.observations.push(...newObservations);
 294 |             results.push({
 295 |                 entityName: observation.entityName,
 296 |                 addedObservations: newObservations
 297 |             });
 298 |         }
 299 |         await this.saveGraph(graph);
 300 |         return results;
 301 |     }
 302 |     async deleteEntities(entityNames) {
 303 |         const graph = await this.loadGraph();
 304 |         // Remove the entities
 305 |         graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
 306 |         // Remove relations that involve the deleted entities
 307 |         graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
 308 |         await this.saveGraph(graph);
 309 |     }
 310 |     async deleteObservations(deletions) {
 311 |         const graph = await this.loadGraph();
 312 |         for (const deletion of deletions) {
 313 |             const entity = graph.entities.find(e => e.name === deletion.entityName);
 314 |             if (entity) {
 315 |                 // Remove the specified observations
 316 |                 entity.observations = entity.observations.filter(o => !deletion.observations.includes(o));
 317 |             }
 318 |         }
 319 |         await this.saveGraph(graph);
 320 |     }
 321 |     async deleteRelations(relations) {
 322 |         const graph = await this.loadGraph();
 323 |         // Remove specified relations
 324 |         graph.relations = graph.relations.filter(r => !relations.some(toDelete => r.from === toDelete.from &&
 325 |             r.to === toDelete.to &&
 326 |             r.relationType === toDelete.relationType));
 327 |         await this.saveGraph(graph);
 328 |     }
 329 |     async readGraph() {
 330 |         return this.loadGraph();
 331 |     }
 332 |     async searchNodes(query) {
 333 |         const graph = await this.loadGraph();
 334 |         // Split query into search terms
 335 |         const terms = query.toLowerCase().split(/\s+/);
 336 |         // Find matching entities
 337 |         const matchingEntityNames = new Set();
 338 |         for (const entity of graph.entities) {
 339 |             // Check if all terms match
 340 |             const matchesAllTerms = terms.every(term => {
 341 |                 // Check entity name
 342 |                 if (entity.name.toLowerCase().includes(term)) {
 343 |                     return true;
 344 |                 }
 345 |                 // Check entity type
 346 |                 if (entity.entityType.toLowerCase().includes(term)) {
 347 |                     return true;
 348 |                 }
 349 |                 // Check observations
 350 |                 for (const observation of entity.observations) {
 351 |                     if (observation.toLowerCase().includes(term)) {
 352 |                         return true;
 353 |                     }
 354 |                 }
 355 |                 return false;
 356 |             });
 357 |             if (matchesAllTerms) {
 358 |                 matchingEntityNames.add(entity.name);
 359 |             }
 360 |         }
 361 |         // Find relations between matching entities
 362 |         const matchingRelations = graph.relations.filter(r => matchingEntityNames.has(r.from) && matchingEntityNames.has(r.to));
 363 |         // Return matching entities and their relations
 364 |         return {
 365 |             entities: graph.entities.filter(e => matchingEntityNames.has(e.name)),
 366 |             relations: matchingRelations
 367 |         };
 368 |     }
 369 |     async openNodes(names) {
 370 |         const graph = await this.loadGraph();
 371 |         // Find the specified entities
 372 |         const entities = graph.entities.filter(e => names.includes(e.name));
 373 |         // Find relations between the specified entities
 374 |         const relations = graph.relations.filter(r => names.includes(r.from) && names.includes(r.to));
 375 |         return {
 376 |             entities,
 377 |             relations
 378 |         };
 379 |     }
 380 |     // Get project overview including research questions, methodology, participants, data sources
 381 |     async getProjectOverview(projectName) {
 382 |         const graph = await this.loadGraph();
 383 |         // Find the project
 384 |         const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
 385 |         if (!project) {
 386 |             throw new Error(`Project '${projectName}' not found`);
 387 |         }
 388 |         // Find research questions
 389 |         const researchQuestions = [];
 390 |         for (const relation of graph.relations) {
 391 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 392 |                 const question = graph.entities.find(e => e.name === relation.from && e.entityType === 'researchQuestion');
 393 |                 if (question) {
 394 |                     researchQuestions.push(question);
 395 |                 }
 396 |             }
 397 |         }
 398 |         // Find participants
 399 |         const participants = [];
 400 |         for (const relation of graph.relations) {
 401 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 402 |                 const participant = graph.entities.find(e => e.name === relation.from && e.entityType === 'participant');
 403 |                 if (participant) {
 404 |                     participants.push(participant);
 405 |                 }
 406 |             }
 407 |         }
 408 |         // Find data sources (interviews, observations, documents)
 409 |         const interviews = [];
 410 |         const observations = [];
 411 |         const documents = [];
 412 |         for (const relation of graph.relations) {
 413 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 414 |                 const entity = graph.entities.find(e => e.name === relation.from);
 415 |                 if (entity) {
 416 |                     if (entity.entityType === 'interview') {
 417 |                         interviews.push(entity);
 418 |                     }
 419 |                     else if (entity.entityType === 'observation') {
 420 |                         observations.push(entity);
 421 |                     }
 422 |                     else if (entity.entityType === 'document') {
 423 |                         documents.push(entity);
 424 |                     }
 425 |                 }
 426 |             }
 427 |         }
 428 |         // Find findings
 429 |         const findings = [];
 430 |         for (const relation of graph.relations) {
 431 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 432 |                 const finding = graph.entities.find(e => e.name === relation.from && e.entityType === 'finding');
 433 |                 if (finding) {
 434 |                     findings.push(finding);
 435 |                 }
 436 |             }
 437 |         }
 438 |         // Find themes
 439 |         const themes = [];
 440 |         for (const relation of graph.relations) {
 441 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 442 |                 const theme = graph.entities.find(e => e.name === relation.from && e.entityType === 'theme');
 443 |                 if (theme) {
 444 |                     themes.push(theme);
 445 |                 }
 446 |             }
 447 |         }
 448 |         // Get methodology info from project observations
 449 |         const methodologyObs = project.observations.filter(o => o.toLowerCase().includes('method') || o.toLowerCase().includes('approach'));
 450 |         return {
 451 |             project,
 452 |             researchQuestions,
 453 |             methodology: methodologyObs,
 454 |             dataCollection: {
 455 |                 participants: participants.length,
 456 |                 interviews: interviews.length,
 457 |                 observations: observations.length,
 458 |                 documents: documents.length,
 459 |                 participantsList: participants,
 460 |                 interviewsList: interviews,
 461 |                 observationsList: observations,
 462 |                 documentsList: documents
 463 |             },
 464 |             analysis: {
 465 |                 themes: themes.length,
 466 |                 themesList: themes
 467 |             },
 468 |             findings
 469 |         };
 470 |     }
 471 |     // Get all data related to a specific participant
 472 |     async getParticipantProfile(participantName) {
 473 |         const graph = await this.loadGraph();
 474 |         // Find the participant
 475 |         const participant = graph.entities.find(e => e.name === participantName && e.entityType === 'participant');
 476 |         if (!participant) {
 477 |             throw new Error(`Participant '${participantName}' not found`);
 478 |         }
 479 |         // Find interviews with this participant
 480 |         const interviews = [];
 481 |         for (const relation of graph.relations) {
 482 |             if (relation.relationType === 'participated_in' && relation.from === participantName) {
 483 |                 const interview = graph.entities.find(e => e.name === relation.to && e.entityType === 'interview');
 484 |                 if (interview) {
 485 |                     interviews.push(interview);
 486 |                 }
 487 |             }
 488 |         }
 489 |         // Find observations including this participant
 490 |         const observations = [];
 491 |         for (const relation of graph.relations) {
 492 |             if (relation.relationType === 'participated_in' && relation.from === participantName) {
 493 |                 const observation = graph.entities.find(e => e.name === relation.to && e.entityType === 'observation');
 494 |                 if (observation) {
 495 |                     observations.push(observation);
 496 |                 }
 497 |             }
 498 |         }
 499 |         // Find quotes from this participant
 500 |         const quotes = [];
 501 |         for (const relation of graph.relations) {
 502 |             if (relation.relationType === 'contains' && relation.to === participantName) {
 503 |                 const quote = graph.entities.find(e => e.name === relation.from && e.entityType === 'quote');
 504 |                 if (quote) {
 505 |                     quotes.push(quote);
 506 |                 }
 507 |             }
 508 |         }
 509 |         // Find any memos about this participant
 510 |         const memos = [];
 511 |         for (const relation of graph.relations) {
 512 |             if (relation.relationType === 'reflects_on' && relation.to === participantName) {
 513 |                 const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
 514 |                 if (memo) {
 515 |                     memos.push(memo);
 516 |                 }
 517 |             }
 518 |         }
 519 |         // Extract demographic information from observations
 520 |         const demographicObs = participant.observations.filter(o => o.toLowerCase().includes('age') ||
 521 |             o.toLowerCase().includes('gender') ||
 522 |             o.toLowerCase().includes('occupation') ||
 523 |             o.toLowerCase().includes('education'));
 524 |         return {
 525 |             participant,
 526 |             demographics: demographicObs,
 527 |             interviews,
 528 |             observations,
 529 |             quotes,
 530 |             memos
 531 |         };
 532 |     }
 533 |     // Get themes with supporting codes and data
 534 |     async getThematicAnalysis(projectName) {
 535 |         const graph = await this.loadGraph();
 536 |         // Find the project
 537 |         const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
 538 |         if (!project) {
 539 |             throw new Error(`Project '${projectName}' not found`);
 540 |         }
 541 |         // Find all themes related to this project
 542 |         const themes = [];
 543 |         for (const relation of graph.relations) {
 544 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 545 |                 const theme = graph.entities.find(e => e.name === relation.from && e.entityType === 'theme');
 546 |                 if (theme) {
 547 |                     themes.push(theme);
 548 |                 }
 549 |             }
 550 |         }
 551 |         // For each theme, find supporting data
 552 |         const thematicAnalysis = themes.map(theme => {
 553 |             // Find codes supporting this theme
 554 |             const supportingCodes = [];
 555 |             for (const relation of graph.relations) {
 556 |                 if (relation.relationType === 'supports' && relation.to === theme.name) {
 557 |                     const code = graph.entities.find(e => e.name === relation.from && e.entityType === 'code');
 558 |                     if (code) {
 559 |                         supportingCodes.push(code);
 560 |                     }
 561 |                 }
 562 |             }
 563 |             // For each code, find supporting quotes
 564 |             const codeData = supportingCodes.map(code => {
 565 |                 const quotes = [];
 566 |                 for (const relation of graph.relations) {
 567 |                     if (relation.relationType === 'codes' && relation.from === code.name) {
 568 |                         const quote = graph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
 569 |                         if (quote) {
 570 |                             quotes.push(quote);
 571 |                         }
 572 |                     }
 573 |                 }
 574 |                 return {
 575 |                     code,
 576 |                     quotes
 577 |                 };
 578 |             });
 579 |             // Find any memos reflecting on this theme
 580 |             const memos = [];
 581 |             for (const relation of graph.relations) {
 582 |                 if (relation.relationType === 'reflects_on' && relation.to === theme.name) {
 583 |                     const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
 584 |                     if (memo) {
 585 |                         memos.push(memo);
 586 |                     }
 587 |                 }
 588 |             }
 589 |             // Find status of the theme
 590 |             const statusObs = theme.observations.find(o => o.startsWith('Status:'));
 591 |             const status = statusObs ? statusObs.split(':')[1].trim() : 'unknown';
 592 |             return {
 593 |                 theme,
 594 |                 status,
 595 |                 supportingData: codeData,
 596 |                 codes: supportingCodes,
 597 |                 memos
 598 |             };
 599 |         });
 600 |         return {
 601 |             project,
 602 |             themes: thematicAnalysis
 603 |         };
 604 |     }
 605 |     // Get all data segments tagged with a specific code
 606 |     async getCodedData(codeName) {
 607 |         const graph = await this.loadGraph();
 608 |         // Find the code
 609 |         const code = graph.entities.find(e => e.name === codeName && e.entityType === 'code');
 610 |         if (!code) {
 611 |             throw new Error(`Code '${codeName}' not found`);
 612 |         }
 613 |         // Find which code group this code belongs to, if any
 614 |         const codeGroups = [];
 615 |         for (const relation of graph.relations) {
 616 |             if (relation.relationType === 'contains' && relation.to === codeName) {
 617 |                 const codeGroup = graph.entities.find(e => e.name === relation.from && e.entityType === 'codeGroup');
 618 |                 if (codeGroup) {
 619 |                     codeGroups.push(codeGroup);
 620 |                 }
 621 |             }
 622 |         }
 623 |         // Find all quotes tagged with this code
 624 |         const quotes = [];
 625 |         for (const relation of graph.relations) {
 626 |             if (relation.relationType === 'codes' && relation.from === codeName) {
 627 |                 const quote = graph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
 628 |                 if (quote) {
 629 |                     quotes.push(quote);
 630 |                 }
 631 |             }
 632 |         }
 633 |         // Find which sources (interviews, observations, documents) these quotes come from
 634 |         const sources = new Map();
 635 |         for (const quote of quotes) {
 636 |             for (const relation of graph.relations) {
 637 |                 if (relation.relationType === 'contains' && relation.from !== codeName && relation.to === quote.name) {
 638 |                     const source = graph.entities.find(e => e.name === relation.from);
 639 |                     if (source) {
 640 |                         sources.set(source.name, source);
 641 |                     }
 642 |                 }
 643 |             }
 644 |         }
 645 |         // Find themes this code supports
 646 |         const themes = [];
 647 |         for (const relation of graph.relations) {
 648 |             if (relation.relationType === 'supports' && relation.from === codeName) {
 649 |                 const theme = graph.entities.find(e => e.name === relation.to && e.entityType === 'theme');
 650 |                 if (theme) {
 651 |                     themes.push(theme);
 652 |                 }
 653 |             }
 654 |         }
 655 |         // Find any memos reflecting on this code
 656 |         const memos = [];
 657 |         for (const relation of graph.relations) {
 658 |             if (relation.relationType === 'reflects_on' && relation.to === codeName) {
 659 |                 const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
 660 |                 if (memo) {
 661 |                     memos.push(memo);
 662 |                 }
 663 |             }
 664 |         }
 665 |         return {
 666 |             code,
 667 |             codeGroups,
 668 |             quotes,
 669 |             sourceCount: sources.size,
 670 |             sources: Array.from(sources.values()),
 671 |             themes,
 672 |             memos
 673 |         };
 674 |     }
 675 |     // Shows data organized by research questions with findings
 676 |     async getResearchQuestionAnalysis(projectName) {
 677 |         const graph = await this.loadGraph();
 678 |         // Find the project
 679 |         const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
 680 |         if (!project) {
 681 |             throw new Error(`Project '${projectName}' not found`);
 682 |         }
 683 |         // Find all research questions for this project
 684 |         const researchQuestions = [];
 685 |         for (const relation of graph.relations) {
 686 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 687 |                 const question = graph.entities.find(e => e.name === relation.from && e.entityType === 'researchQuestion');
 688 |                 if (question) {
 689 |                     researchQuestions.push(question);
 690 |                 }
 691 |             }
 692 |         }
 693 |         // For each research question, find related data
 694 |         const questionAnalysis = researchQuestions.map(question => {
 695 |             // Find findings that answer this question
 696 |             const findings = [];
 697 |             for (const relation of graph.relations) {
 698 |                 if (relation.relationType === 'answers' && relation.to === question.name) {
 699 |                     const finding = graph.entities.find(e => e.name === relation.from && e.entityType === 'finding');
 700 |                     if (finding) {
 701 |                         findings.push(finding);
 702 |                     }
 703 |                 }
 704 |             }
 705 |             // Find themes related to this question
 706 |             const themes = [];
 707 |             for (const relation of graph.relations) {
 708 |                 if (relation.relationType === 'answers' && relation.to === question.name) {
 709 |                     const theme = graph.entities.find(e => e.name === relation.from && e.entityType === 'theme');
 710 |                     if (theme) {
 711 |                         themes.push(theme);
 712 |                     }
 713 |                 }
 714 |             }
 715 |             // Find data directly addressing this question
 716 |             const quotes = [];
 717 |             for (const relation of graph.relations) {
 718 |                 if (relation.relationType === 'answers' && relation.to === question.name) {
 719 |                     const quote = graph.entities.find(e => e.name === relation.from && e.entityType === 'quote');
 720 |                     if (quote) {
 721 |                         quotes.push(quote);
 722 |                     }
 723 |                 }
 724 |             }
 725 |             return {
 726 |                 question,
 727 |                 findings,
 728 |                 themes,
 729 |                 quotes
 730 |             };
 731 |         });
 732 |         return {
 733 |             project,
 734 |             researchQuestions: questionAnalysis
 735 |         };
 736 |     }
 737 |     // Returns data in temporal sequence
 738 |     async getChronologicalData(projectName, dataType) {
 739 |         const graph = await this.loadGraph();
 740 |         // Find the project
 741 |         const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
 742 |         if (!project) {
 743 |             throw new Error(`Project '${projectName}' not found`);
 744 |         }
 745 |         // Find all data collection entities for this project
 746 |         let dataEntities = [];
 747 |         for (const relation of graph.relations) {
 748 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 749 |                 let entity;
 750 |                 // Filter by data type if specified
 751 |                 if (dataType) {
 752 |                     entity = graph.entities.find(e => e.name === relation.from && e.entityType === dataType);
 753 |                 }
 754 |                 else {
 755 |                     entity = graph.entities.find(e => e.name === relation.from &&
 756 |                         (e.entityType === 'interview' ||
 757 |                             e.entityType === 'observation' ||
 758 |                             e.entityType === 'document'));
 759 |                 }
 760 |                 if (entity) {
 761 |                     dataEntities.push(entity);
 762 |                 }
 763 |             }
 764 |         }
 765 |         // Extract date information for each entity
 766 |         const dataWithDates = dataEntities.map(entity => {
 767 |             const dateObs = entity.observations.find(o => o.startsWith('Date:') || o.startsWith('Collected on:') || o.startsWith('Created:'));
 768 |             let date = new Date(0);
 769 |             if (dateObs) {
 770 |                 // Extract date string and try to parse it
 771 |                 const dateString = dateObs.split(':')[1].trim();
 772 |                 const parsedDate = new Date(dateString);
 773 |                 if (!isNaN(parsedDate.getTime())) {
 774 |                     date = parsedDate;
 775 |                 }
 776 |             }
 777 |             return {
 778 |                 entity,
 779 |                 date
 780 |             };
 781 |         });
 782 |         // Sort by date
 783 |         dataWithDates.sort((a, b) => a.date.getTime() - b.date.getTime());
 784 |         // Create a timeline of data
 785 |         const timeline = dataWithDates.map(item => {
 786 |             // For each entity, find related quotes
 787 |             const quotes = [];
 788 |             for (const relation of graph.relations) {
 789 |                 if (relation.relationType === 'contains' && relation.from === item.entity.name) {
 790 |                     const quote = graph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
 791 |                     if (quote) {
 792 |                         quotes.push(quote);
 793 |                     }
 794 |                 }
 795 |             }
 796 |             return {
 797 |                 date: item.date,
 798 |                 entity: item.entity,
 799 |                 quotes
 800 |             };
 801 |         });
 802 |         return {
 803 |             project,
 804 |             timeline
 805 |         };
 806 |     }
 807 |     // Finds where multiple codes appear together
 808 |     async getCodeCooccurrence(codeName) {
 809 |         const graph = await this.loadGraph();
 810 |         // Find the code
 811 |         const code = graph.entities.find(e => e.name === codeName && e.entityType === 'code');
 812 |         if (!code) {
 813 |             throw new Error(`Code '${codeName}' not found`);
 814 |         }
 815 |         // Find all quotes tagged with this code
 816 |         const quotes = [];
 817 |         for (const relation of graph.relations) {
 818 |             if (relation.relationType === 'codes' && relation.from === codeName) {
 819 |                 const quote = graph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
 820 |                 if (quote) {
 821 |                     quotes.push(quote);
 822 |                 }
 823 |             }
 824 |         }
 825 |         // For each quote, find other codes that also tag it
 826 |         const codeOccurrences = new Map();
 827 |         for (const quote of quotes) {
 828 |             for (const relation of graph.relations) {
 829 |                 if (relation.relationType === 'codes' && relation.from !== codeName && relation.to === quote.name) {
 830 |                     const otherCode = graph.entities.find(e => e.name === relation.from && e.entityType === 'code');
 831 |                     if (otherCode) {
 832 |                         if (codeOccurrences.has(otherCode.name)) {
 833 |                             const occurrence = codeOccurrences.get(otherCode.name);
 834 |                             occurrence.count++;
 835 |                             occurrence.quotes.push(quote);
 836 |                         }
 837 |                         else {
 838 |                             codeOccurrences.set(otherCode.name, {
 839 |                                 code: otherCode,
 840 |                                 count: 1,
 841 |                                 quotes: [quote]
 842 |                             });
 843 |                         }
 844 |                     }
 845 |                 }
 846 |             }
 847 |         }
 848 |         // Sort codes by co-occurrence frequency
 849 |         const cooccurringCodes = Array.from(codeOccurrences.values())
 850 |             .sort((a, b) => b.count - a.count);
 851 |         return {
 852 |             code,
 853 |             quotesCount: quotes.length,
 854 |             cooccurringCodes
 855 |         };
 856 |     }
 857 |     // Gets all memos related to a specific entity
 858 |     async getMemosByFocus(entityName) {
 859 |         const graph = await this.loadGraph();
 860 |         // Find the entity
 861 |         const entity = graph.entities.find(e => e.name === entityName);
 862 |         if (!entity) {
 863 |             throw new Error(`Entity '${entityName}' not found`);
 864 |         }
 865 |         // Find all memos reflecting on this entity
 866 |         const memos = [];
 867 |         for (const relation of graph.relations) {
 868 |             if (relation.relationType === 'reflects_on' && relation.to === entityName) {
 869 |                 const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
 870 |                 if (memo) {
 871 |                     memos.push(memo);
 872 |                 }
 873 |             }
 874 |         }
 875 |         // Sort memos by date if possible
 876 |         const memosWithDates = memos.map(memo => {
 877 |             const dateObs = memo.observations.find(o => o.startsWith('Date:') || o.startsWith('Created:'));
 878 |             let date = new Date(0);
 879 |             if (dateObs) {
 880 |                 const dateString = dateObs.split(':')[1].trim();
 881 |                 const parsedDate = new Date(dateString);
 882 |                 if (!isNaN(parsedDate.getTime())) {
 883 |                     date = parsedDate;
 884 |                 }
 885 |             }
 886 |             return {
 887 |                 memo,
 888 |                 date
 889 |             };
 890 |         });
 891 |         // Sort by date, most recent first
 892 |         memosWithDates.sort((a, b) => b.date.getTime() - a.date.getTime());
 893 |         return {
 894 |             entity,
 895 |             memos: memosWithDates.map(m => m.memo)
 896 |         };
 897 |     }
 898 |     // Returns information about methods, sampling, analysis approach
 899 |     async getMethodologyDetails(projectName) {
 900 |         const graph = await this.loadGraph();
 901 |         // Find the project
 902 |         const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
 903 |         if (!project) {
 904 |             throw new Error(`Project '${projectName}' not found`);
 905 |         }
 906 |         // Extract methodology information from project observations
 907 |         const methodologyObs = project.observations.filter(o => o.toLowerCase().includes('method') ||
 908 |             o.toLowerCase().includes('approach') ||
 909 |             o.toLowerCase().includes('sampling') ||
 910 |             o.toLowerCase().includes('analysis') ||
 911 |             o.toLowerCase().includes('validity') ||
 912 |             o.toLowerCase().includes('reliability'));
 913 |         // Find methodology-related memos
 914 |         const memos = [];
 915 |         for (const relation of graph.relations) {
 916 |             if (relation.relationType === 'reflects_on' && relation.to === projectName) {
 917 |                 const memo = graph.entities.find(e => e.name === relation.from && e.entityType === 'memo');
 918 |                 if (memo && memo.observations.some(o => o.toLowerCase().includes('method') ||
 919 |                     o.toLowerCase().includes('approach') ||
 920 |                     o.toLowerCase().includes('sampling') ||
 921 |                     o.toLowerCase().includes('analysis'))) {
 922 |                     memos.push(memo);
 923 |                 }
 924 |             }
 925 |         }
 926 |         // Calculate data collection statistics
 927 |         // 1. Get all interviews
 928 |         const interviews = [];
 929 |         for (const relation of graph.relations) {
 930 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 931 |                 const interview = graph.entities.find(e => e.name === relation.from && e.entityType === 'interview');
 932 |                 if (interview) {
 933 |                     interviews.push(interview);
 934 |                 }
 935 |             }
 936 |         }
 937 |         // 2. Get all observations
 938 |         const observations = [];
 939 |         for (const relation of graph.relations) {
 940 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 941 |                 const observation = graph.entities.find(e => e.name === relation.from && e.entityType === 'observation');
 942 |                 if (observation) {
 943 |                     observations.push(observation);
 944 |                 }
 945 |             }
 946 |         }
 947 |         // 3. Get all documents
 948 |         const documents = [];
 949 |         for (const relation of graph.relations) {
 950 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 951 |                 const document = graph.entities.find(e => e.name === relation.from && e.entityType === 'document');
 952 |                 if (document) {
 953 |                     documents.push(document);
 954 |                 }
 955 |             }
 956 |         }
 957 |         // 4. Get all participants
 958 |         const participants = [];
 959 |         for (const relation of graph.relations) {
 960 |             if (relation.relationType === 'part_of' && relation.to === projectName) {
 961 |                 const participant = graph.entities.find(e => e.name === relation.from && e.entityType === 'participant');
 962 |                 if (participant) {
 963 |                     participants.push(participant);
 964 |                 }
 965 |             }
 966 |         }
 967 |         // Find literature cited in this project
 968 |         const literature = [];
 969 |         for (const relation of graph.relations) {
 970 |             if (relation.relationType === 'cites' && relation.from === projectName) {
 971 |                 const source = graph.entities.find(e => e.name === relation.to && e.entityType === 'literature');
 972 |                 if (source) {
 973 |                     literature.push(source);
 974 |                 }
 975 |             }
 976 |         }
 977 |         return {
 978 |             project,
 979 |             methodology: methodologyObs,
 980 |             dataCollection: {
 981 |                 participants: participants.length,
 982 |                 interviews: interviews.length,
 983 |                 observations: observations.length,
 984 |                 documents: documents.length
 985 |             },
 986 |             memos,
 987 |             literature
 988 |         };
 989 |     }
 990 |     // First, let's add the missing getRelatedEntities method to the KnowledgeGraphManager class
 991 |     // Add this before the async main() function
 992 |     async getRelatedEntities(entityName, relationTypes) {
 993 |         const graph = await this.loadGraph();
 994 |         // Find the entity
 995 |         const entity = graph.entities.find(e => e.name === entityName);
 996 |         if (!entity) {
 997 |             throw new Error(`Entity '${entityName}' not found`);
 998 |         }
 999 |         // Find all relations involving this entity
1000 |         let relevantRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
1001 |         // Filter by relation types if specified
1002 |         if (relationTypes && relationTypes.length > 0) {
1003 |             relevantRelations = relevantRelations.filter(r => relationTypes.includes(r.relationType));
1004 |         }
1005 |         // Get all related entities grouped by relation type
1006 |         const related = {};
1007 |         for (const relation of relevantRelations) {
1008 |             const relationType = relation.relationType;
1009 |             if (!related[relationType]) {
1010 |                 related[relationType] = [];
1011 |             }
1012 |             if (relation.from === entityName) {
1013 |                 const target = graph.entities.find(e => e.name === relation.to);
1014 |                 if (target) {
1015 |                     related[relationType].push(target);
1016 |                 }
1017 |             }
1018 |             else {
1019 |                 const source = graph.entities.find(e => e.name === relation.from);
1020 |                 if (source) {
1021 |                     related[relationType].push(source);
1022 |                 }
1023 |             }
1024 |         }
1025 |         return {
1026 |             entity,
1027 |             related
1028 |         };
1029 |     }
1030 | }
1031 | // Main function to set up the MCP server
1032 | async function main() {
1033 |     try {
1034 |         const knowledgeGraphManager = new KnowledgeGraphManager();
1035 |         // Initialize status and priority entities
1036 |         await knowledgeGraphManager.initializeStatusAndPriority();
1037 |         // Create the MCP server with a name and version
1038 |         const server = new McpServer({
1039 |             name: "Context Manager",
1040 |             version: "1.0.0"
1041 |         });
1042 |         // Define a resource that exposes the entire graph
1043 |         server.resource("graph", "graph://researcher/qualitative", async (uri) => ({
1044 |             contents: [{
1045 |                     uri: uri.href,
1046 |                     text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2)
1047 |                 }]
1048 |         }));
1049 |         // Define tools using zod for parameter validation
1050 |         /**
1051 |          * Load context for a specific entity
1052 |          */
1053 |         server.tool("loadcontext", toolDescriptions["loadcontext"], {
1054 |             entityName: z.string(),
1055 |             entityType: z.string().optional(),
1056 |             sessionId: z.string().optional() // Optional to maintain backward compatibility
1057 |         }, async ({ entityName, entityType = "project", sessionId }) => {
1058 |             try {
1059 |                 // Validate session if ID is provided
1060 |                 if (sessionId) {
1061 |                     const sessionStates = await loadSessionStates();
1062 |                     if (!sessionStates.has(sessionId)) {
1063 |                         console.warn(`Warning: Session ${sessionId} not found, but proceeding with context load`);
1064 |                         // Initialize it anyway for more robustness
1065 |                         sessionStates.set(sessionId, []);
1066 |                         await saveSessionStates(sessionStates);
1067 |                     }
1068 |                     // Track that this entity was loaded in this session
1069 |                     const sessionState = sessionStates.get(sessionId) || [];
1070 |                     const loadEvent = {
1071 |                         type: 'context_loaded',
1072 |                         timestamp: new Date().toISOString(),
1073 |                         entityName,
1074 |                         entityType
1075 |                     };
1076 |                     sessionState.push(loadEvent);
1077 |                     sessionStates.set(sessionId, sessionState);
1078 |                     await saveSessionStates(sessionStates);
1079 |                 }
1080 |                 // Get the entity
1081 |                 // Changed from using 'name:' prefix to directly searching by the entity name
1082 |                 const entityGraph = await knowledgeGraphManager.searchNodes(entityName);
1083 |                 if (entityGraph.entities.length === 0) {
1084 |                     throw new Error(`Entity ${entityName} not found`);
1085 |                 }
1086 |                 // Find the exact entity by name (case-sensitive match)
1087 |                 const entity = entityGraph.entities.find(e => e.name === entityName);
1088 |                 if (!entity) {
1089 |                     throw new Error(`Entity ${entityName} not found`);
1090 |                 }
1091 |                 // Different context loading based on entity type
1092 |                 let contextMessage = "";
1093 |                 if (entityType === "project") {
1094 |                     // Get project overview
1095 |                     const projectOverview = await knowledgeGraphManager.getProjectOverview(entityName);
1096 |                     // Get thematic analysis
1097 |                     let thematicAnalysis;
1098 |                     try {
1099 |                         thematicAnalysis = await knowledgeGraphManager.getThematicAnalysis(entityName);
1100 |                     }
1101 |                     catch (error) {
1102 |                         thematicAnalysis = { themes: [] };
1103 |                     }
1104 |                     // Get research question analysis
1105 |                     let researchQuestions;
1106 |                     try {
1107 |                         researchQuestions = await knowledgeGraphManager.getResearchQuestionAnalysis(entityName);
1108 |                     }
1109 |                     catch (error) {
1110 |                         researchQuestions = { researchQuestions: [] };
1111 |                     }
1112 |                     // Get methodology details
1113 |                     let methodology;
1114 |                     try {
1115 |                         methodology = await knowledgeGraphManager.getMethodologyDetails(entityName);
1116 |                     }
1117 |                     catch (error) {
1118 |                         methodology = { methodology: [] };
1119 |                     }
1120 |                     // Get status and priority using the relation-based approach
1121 |                     const status = await knowledgeGraphManager.getEntityStatus(entityName) || "Unknown";
1122 |                     const priority = await knowledgeGraphManager.getEntityPriority(entityName);
1123 |                     const priorityText = priority ? `- **Priority**: ${priority}` : "";
1124 |                     // Format observations
1125 |                     const observationsList = entity.observations.length > 0
1126 |                         ? entity.observations.map(obs => `- ${obs}`).join("\n")
1127 |                         : "No observations";
1128 |                     // Extract methodology information
1129 |                     const methodologyText = methodology.methodology?.map((m) => `- ${m}`).join("\n") || "No methodology details available";
1130 |                     // Extract research questions
1131 |                     const questionsText = researchQuestions.researchQuestions?.map((q) => {
1132 |                         const findings = q.findings?.map((f) => `  - ${f.name}`).join("\n") || "  - No findings yet";
1133 |                         return `- **${q.question.name}**\n${findings}`;
1134 |                     }).join("\n") || "No research questions found";
1135 |                     // Format data collection stats
1136 |                     const participantCount = projectOverview.dataCollection?.participants || 0;
1137 |                     const interviewCount = projectOverview.dataCollection?.interviews || 0;
1138 |                     const observationCount = projectOverview.dataCollection?.observations || 0;
1139 |                     const documentCount = projectOverview.dataCollection?.documents || 0;
1140 |                     // Format theme analysis
1141 |                     const themesText = thematicAnalysis.themes?.map(async (t) => {
1142 |                         const codeCount = t.supportingData?.length || 0;
1143 |                         const themeStatus = await knowledgeGraphManager.getEntityStatus(t.theme.name) || "unknown";
1144 |                         return `- **${t.theme.name}** (Status: ${themeStatus}): ${codeCount} codes`;
1145 |                     });
1146 |                     const resolvedThemesText = themesText ?
1147 |                         await Promise.all(themesText).then(texts => texts.join("\n")) :
1148 |                         "No themes identified yet";
1149 |                     // Get recent data collection - without date references
1150 |                     const recentInterviews = projectOverview.dataCollection?.interviewsList?.slice(0, 5).map(async (i) => {
1151 |                         const participant = i.observations.find(o => o.startsWith("participant:"))?.substring(12) || "Unknown";
1152 |                         const interviewStatus = await knowledgeGraphManager.getEntityStatus(i.name) || "unknown";
1153 |                         return `- **${i.name}** with ${participant} (Status: ${interviewStatus})`;
1154 |                     });
1155 |                     const resolvedInterviewsText = recentInterviews ?
1156 |                         await Promise.all(recentInterviews).then(texts => texts.join("\n")) :
1157 |                         "No recent interviews";
1158 |                     // Get findings with status from relations
1159 |                     const findingsText = projectOverview.findings?.map(async (f) => {
1160 |                         const findingStatus = await knowledgeGraphManager.getEntityStatus(f.name) || "preliminary";
1161 |                         const findingObs = f.observations.length > 0 ? f.observations[0] : "No description";
1162 |                         return `- **${f.name}** (Status: ${findingStatus}): ${findingObs}`;
1163 |                     });
1164 |                     const resolvedFindingsText = findingsText ?
1165 |                         await Promise.all(findingsText).then(texts => texts.join("\n")) :
1166 |                         "No findings recorded yet";
1167 |                     contextMessage = `# Qualitative Research Project Context: ${entityName}
1168 | 
1169 | ## Project Details
1170 | - **Status**: ${status}
1171 | ${priorityText}
1172 | 
1173 | ## Observations
1174 | ${observationsList}
1175 | 
1176 | ## Research Design
1177 | ${methodologyText}
1178 | 
1179 | ## Research Questions
1180 | ${questionsText}
1181 | 
1182 | ## Data Collection Stats
1183 | - **Participants**: ${participantCount}
1184 | - **Interviews**: ${interviewCount}
1185 | - **Observations**: ${observationCount}
1186 | - **Documents**: ${documentCount}
1187 | 
1188 | ## Recent Interviews
1189 | ${resolvedInterviewsText}
1190 | 
1191 | ## Analysis Progress
1192 | ### Themes
1193 | ${resolvedThemesText}
1194 | 
1195 | ## Findings
1196 | ${resolvedFindingsText}`;
1197 |                 }
1198 |                 else if (entityType === "participant") {
1199 |                     // Get participant profile
1200 |                     const participantProfile = await knowledgeGraphManager.getParticipantProfile(entityName);
1201 |                     // Get status and priority using the relation-based approach
1202 |                     const status = await knowledgeGraphManager.getEntityStatus(entityName) || "Unknown";
1203 |                     const priority = await knowledgeGraphManager.getEntityPriority(entityName);
1204 |                     const priorityText = priority ? `- **Priority**: ${priority}` : "";
1205 |                     // Format observations
1206 |                     const observationsList = entity.observations.length > 0
1207 |                         ? entity.observations.map(obs => `- ${obs}`).join("\n")
1208 |                         : "No observations";
1209 |                     // Format demographics without relying on patterns
1210 |                     const demographics = participantProfile.demographics?.map((d) => `- ${d}`).join("\n") || "No demographic information available";
1211 |                     // Format interviews with status from relations
1212 |                     const interviewsText = participantProfile.interviews?.map(async (i) => {
1213 |                         const interviewStatus = await knowledgeGraphManager.getEntityStatus(i.name) || "unknown";
1214 |                         return `- **${i.name}** (Status: ${interviewStatus})`;
1215 |                     });
1216 |                     const resolvedInterviewsText = interviewsText ?
1217 |                         await Promise.all(interviewsText).then(texts => texts.join("\n")) :
1218 |                         "No interviews recorded";
1219 |                     // Format observations with status from relations
1220 |                     const observationsText = participantProfile.observations?.map(async (o) => {
1221 |                         const observationStatus = await knowledgeGraphManager.getEntityStatus(o.name) || "unknown";
1222 |                         return `- **${o.name}** (Status: ${observationStatus})`;
1223 |                     });
1224 |                     const resolvedObservationsText = observationsText ?
1225 |                         await Promise.all(observationsText).then(texts => texts.join("\n")) :
1226 |                         "No observations recorded";
1227 |                     // Format quotes
1228 |                     const quotesText = participantProfile.quotes?.map((q) => {
1229 |                         // Show the full quote
1230 |                         const quote = q.observations.find(o => !o.startsWith("source:") && !o.startsWith("context:"));
1231 |                         const source = q.observations.find(o => o.startsWith("source:"))?.substring(7) || "Unknown source";
1232 |                         return `- "${quote || "No text available"}" (Source: ${source})`;
1233 |                     }).join("\n") || "No quotes recorded";
1234 |                     // Format memos
1235 |                     const memosText = participantProfile.memos?.map(async (m) => {
1236 |                         const memoStatus = await knowledgeGraphManager.getEntityStatus(m.name) || "unknown";
1237 |                         const topic = m.observations.find(o => o.startsWith("topic:"))?.substring(6) || "Untitled";
1238 |                         return `- **${topic}** (Status: ${memoStatus})`;
1239 |                     });
1240 |                     const resolvedMemosText = memosText ?
1241 |                         await Promise.all(memosText).then(texts => texts.join("\n")) :
1242 |                         "No memos about this participant";
1243 |                     contextMessage = `# Participant Context: ${entityName}
1244 | 
1245 | ## Status and Priority
1246 | - **Status**: ${status}
1247 | ${priorityText}
1248 | 
1249 | ## Observations
1250 | ${observationsList}
1251 | 
1252 | ## Demographics
1253 | ${demographics}
1254 | 
1255 | ## Interviews
1256 | ${resolvedInterviewsText}
1257 | 
1258 | ## Observations
1259 | ${resolvedObservationsText}
1260 | 
1261 | ## Quotes
1262 | ${quotesText}
1263 | 
1264 | ## Research Memos
1265 | ${resolvedMemosText}`;
1266 |                 }
1267 |                 else if (entityType === "interview") {
1268 |                     // Find which project this interview belongs to
1269 |                     let projectName = 'Unknown project';
1270 |                     for (const relation of entityGraph.relations) {
1271 |                         if (relation.relationType === 'part_of' && relation.from === entityName) {
1272 |                             const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
1273 |                             if (project) {
1274 |                                 projectName = project.name;
1275 |                                 break;
1276 |                             }
1277 |                         }
1278 |                     }
1279 |                     // Get status and priority using the relation-based approach
1280 |                     const status = await knowledgeGraphManager.getEntityStatus(entityName) || "Unknown";
1281 |                     const priority = await knowledgeGraphManager.getEntityPriority(entityName);
1282 |                     const priorityText = priority ? `- **Priority**: ${priority}` : "";
1283 |                     // Format observations
1284 |                     const observationsList = entity.observations.length > 0
1285 |                         ? entity.observations.map(obs => `- ${obs}`).join("\n")
1286 |                         : "No observations";
1287 |                     // Get interview details without parsing date
1288 |                     const participant = entity.observations.find(o => o.startsWith("participant:"))?.substring(12) || "Unknown";
1289 |                     // Find codes applied to this interview and include their status
1290 |                     const codesWithStatus = [];
1291 |                     for (const relation of entityGraph.relations) {
1292 |                         if (relation.relationType === 'codes' && relation.to === entityName) {
1293 |                             const code = entityGraph.entities.find(e => e.name === relation.from && e.entityType === 'code');
1294 |                             if (code) {
1295 |                                 const codeStatus = await knowledgeGraphManager.getEntityStatus(code.name) || "unknown";
1296 |                                 codesWithStatus.push({
1297 |                                     code,
1298 |                                     status: codeStatus
1299 |                                 });
1300 |                             }
1301 |                         }
1302 |                     }
1303 |                     const codesText = codesWithStatus.map(c => `- **${c.code.name}** (Status: ${c.status}): ${c.code.observations[0] || "No description"}`).join("\n") || "No codes applied yet";
1304 |                     // Find quotes from this interview
1305 |                     const quotes = [];
1306 |                     for (const relation of entityGraph.relations) {
1307 |                         if (relation.relationType === 'contains' && relation.from === entityName) {
1308 |                             const quote = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'quote');
1309 |                             if (quote) {
1310 |                                 quotes.push(quote);
1311 |                             }
1312 |                         }
1313 |                     }
1314 |                     const quotesText = quotes.map(q => {
1315 |                         // Get the full quote text
1316 |                         const quoteText = q.observations.find(o => !o.startsWith("context:") && !o.startsWith("speaker:")) || "No text";
1317 |                         return `- "${quoteText}"`;
1318 |                     }).join("\n") || "No notable quotes recorded";
1319 |                     contextMessage = `# Interview Context: ${entityName}
1320 | 
1321 | ## Overview
1322 | - **Project**: ${projectName}
1323 | - **Participant**: ${participant}
1324 | - **Status**: ${status}
1325 | ${priorityText}
1326 | 
1327 | ## Observations
1328 | ${observationsList}
1329 | 
1330 | ## Applied Codes
1331 | ${codesText}
1332 | 
1333 | ## Notable Quotes
1334 | ${quotesText}`;
1335 |                 }
1336 |                 else if (entityType === "code") {
1337 |                     // Get coded data for this code
1338 |                     const codedData = await knowledgeGraphManager.getCodedData(entityName);
1339 |                     // Format code context
1340 |                     const definition = entity.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1341 |                     const created = entity.observations.find(o => o.startsWith("created:"))?.substring(8) || "Unknown";
1342 |                     const status = entity.observations.find(o => o.startsWith("status:"))?.substring(7) || "active";
1343 |                     // Format code groups
1344 |                     const codeGroupsText = codedData.codeGroups?.map((group) => {
1345 |                         const description = group.observations.find(o => !o.startsWith("created:"));
1346 |                         return `- **${group.name}**: ${description || "No description"}`;
1347 |                     }).join("\n") || "Not part of any code groups";
1348 |                     // Format quotes
1349 |                     const quotesText = codedData.quotes?.map((quote) => {
1350 |                         const source = quote.observations.find(o => o.startsWith("source:"))?.substring(7) || "Unknown source";
1351 |                         const text = quote.observations.find(o => !o.startsWith("source:") && !o.startsWith("context:"));
1352 |                         return `- "${text || "No text"}" (Source: ${source})`;
1353 |                     }).join("\n") || "No quotes tagged with this code";
1354 |                     // Format sources
1355 |                     const sourcesText = codedData.sources?.map((source) => {
1356 |                         return `- **${source.name}** (${source.entityType})`;
1357 |                     }).join("\n") || "No sources found";
1358 |                     // Format themes
1359 |                     const themesText = codedData.themes?.map((theme) => {
1360 |                         const description = theme.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1361 |                         return `- **${theme.name}**: ${description || "No description"}`;
1362 |                     }).join("\n") || "Not associated with any themes";
1363 |                     // Get co-occurrence data
1364 |                     let cooccurrenceData;
1365 |                     try {
1366 |                         cooccurrenceData = await knowledgeGraphManager.getCodeCooccurrence(entityName);
1367 |                         // Format co-occurrence
1368 |                         const cooccurrenceText = cooccurrenceData.cooccurringCodes?.map((c) => {
1369 |                             return `- **${c.code.name}** (${c.count} co-occurrences)`;
1370 |                         }).slice(0, 5).join("\n") || "No code co-occurrence data";
1371 |                         contextMessage = `# Code Context: ${entityName}
1372 | 
1373 | ## Code Details
1374 | - **Definition**: ${definition || "No definition provided"}
1375 | - **Created**: ${created}
1376 | - **Status**: ${status}
1377 | - **Items Coded**: ${codedData.quotes?.length || 0}
1378 | 
1379 | ## Part of Code Groups
1380 | ${codeGroupsText}
1381 | 
1382 | ## Supporting Themes
1383 | ${themesText}
1384 | 
1385 | ## Top Co-occurring Codes
1386 | ${cooccurrenceText}
1387 | 
1388 | ## Example Quotes
1389 | ${quotesText}
1390 | 
1391 | ## Used in These Sources
1392 | ${sourcesText}`;
1393 |                     }
1394 |                     catch (error) {
1395 |                         contextMessage = `# Code Context: ${entityName}
1396 | 
1397 | ## Code Details
1398 | - **Definition**: ${definition || "No definition provided"}
1399 | - **Created**: ${created}
1400 | - **Status**: ${status}
1401 | - **Items Coded**: ${codedData.quotes?.length || 0}
1402 | 
1403 | ## Part of Code Groups
1404 | ${codeGroupsText}
1405 | 
1406 | ## Supporting Themes
1407 | ${themesText}
1408 | 
1409 | ## Example Quotes
1410 | ${quotesText}
1411 | 
1412 | ## Used in These Sources
1413 | ${sourcesText}`;
1414 |                     }
1415 |                 }
1416 |                 else if (entityType === "theme") {
1417 |                     // Get thematic analysis data
1418 |                     let projectName = "";
1419 |                     // Find which project this theme belongs to
1420 |                     for (const relation of entityGraph.relations) {
1421 |                         if (relation.relationType === 'part_of' && relation.from === entityName) {
1422 |                             const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
1423 |                             if (project) {
1424 |                                 projectName = project.name;
1425 |                                 break;
1426 |                             }
1427 |                         }
1428 |                     }
1429 |                     let thematicAnalysis;
1430 |                     try {
1431 |                         thematicAnalysis = await knowledgeGraphManager.getThematicAnalysis(projectName);
1432 |                         // Find this theme in the analysis
1433 |                         const themeAnalysis = thematicAnalysis.themes?.find((t) => t.theme.name === entityName);
1434 |                         if (themeAnalysis) {
1435 |                             const description = entity.observations.find(o => !o.startsWith("created:") && !o.startsWith("status:"));
1436 |                             const status = entity.observations.find(o => o.startsWith("status:"))?.substring(7) || "emerging";
1437 |                             const created = entity.observations.find(o => o.startsWith("created:"))?.substring(8) || "Unknown";
1438 |                             // Format codes
1439 |                             const codesText = themeAnalysis.codes?.map((code) => {
1440 |                                 const definition = code.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1441 |                                 return `- **${code.name}**: ${definition || "No definition"}`;
1442 |                             }).join("\n") || "No supporting codes";
1443 |                             // Format supporting quotes
1444 |                             const quotesText = themeAnalysis.supportingData?.flatMap((codeData) => codeData.quotes.map((quote) => {
1445 |                                 const text = quote.observations.find(o => !o.startsWith("source:") && !o.startsWith("context:"));
1446 |                                 return `- "${text || "No text"}" [Code: ${codeData.code.name}]`;
1447 |                             })).slice(0, 10).join("\n") || "No supporting quotes";
1448 |                             // Format memos
1449 |                             const memosText = themeAnalysis.memos?.map((memo) => {
1450 |                                 const date = memo.observations.find(o => o.startsWith("date:"))?.substring(5) || "Unknown date";
1451 |                                 const topic = memo.observations.find(o => o.startsWith("topic:"))?.substring(6) || "Untitled";
1452 |                                 const content = memo.observations.find(o => !o.startsWith("date:") && !o.startsWith("topic:"));
1453 |                                 return `- **${topic}** (${date}): ${content ? (content.length > 100 ? content.substring(0, 100) + "..." : content) : "No content"}`;
1454 |                             }).join("\n") || "No analytical memos about this theme";
1455 |                             contextMessage = `# Theme Context: ${entityName}
1456 | 
1457 | ## Theme Details
1458 | - **Description**: ${description || "No description provided"}
1459 | - **Status**: ${status}
1460 | - **Created**: ${created}
1461 | - **Project**: ${projectName || "Not associated with a specific project"}
1462 | 
1463 | ## Supporting Codes
1464 | ${codesText}
1465 | 
1466 | ## Example Supporting Quotes
1467 | ${quotesText}
1468 | 
1469 | ## Analytical Memos
1470 | ${memosText}`;
1471 |                         }
1472 |                         else {
1473 |                             const description = entity.observations.find(o => !o.startsWith("created:") && !o.startsWith("status:"));
1474 |                             const status = entity.observations.find(o => o.startsWith("status:"))?.substring(7) || "emerging";
1475 |                             contextMessage = `# Theme Context: ${entityName}
1476 | 
1477 | ## Theme Details
1478 | - **Description**: ${description || "No description provided"}
1479 | - **Status**: ${status}
1480 | - **Project**: ${projectName || "Not associated with a specific project"}
1481 | 
1482 | No detailed analysis available for this theme.`;
1483 |                         }
1484 |                     }
1485 |                     catch (error) {
1486 |                         const description = entity.observations.find(o => !o.startsWith("created:") && !o.startsWith("status:"));
1487 |                         const status = entity.observations.find(o => o.startsWith("status:"))?.substring(7) || "emerging";
1488 |                         contextMessage = `# Theme Context: ${entityName}
1489 | 
1490 | ## Theme Details
1491 | - **Description**: ${description || "No description provided"}
1492 | - **Status**: ${status}
1493 | - **Project**: ${projectName || "Not associated with a specific project"}
1494 | 
1495 | No detailed analysis available for this theme.`;
1496 |                     }
1497 |                 }
1498 |                 else if (entityType === "memo") {
1499 |                     // Get memo details
1500 |                     const topic = entity.observations.find(o => o.startsWith("topic:"))?.substring(6) || "Untitled";
1501 |                     const date = entity.observations.find(o => o.startsWith("date:"))?.substring(5) || "Unknown date";
1502 |                     const content = entity.observations.find(o => !o.startsWith("topic:") && !o.startsWith("date:"));
1503 |                     // Find what this memo reflects on
1504 |                     const relatedEntities = [];
1505 |                     for (const relation of entityGraph.relations) {
1506 |                         if (relation.relationType === 'reflects_on' && relation.from === entityName) {
1507 |                             const relatedEntity = entityGraph.entities.find(e => e.name === relation.to);
1508 |                             if (relatedEntity) {
1509 |                                 relatedEntities.push(relatedEntity);
1510 |                             }
1511 |                         }
1512 |                     }
1513 |                     // Find which project this memo belongs to
1514 |                     let projectName = 'Unknown project';
1515 |                     for (const relation of entityGraph.relations) {
1516 |                         if (relation.relationType === 'part_of' && relation.from === entityName) {
1517 |                             const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
1518 |                             if (project) {
1519 |                                 projectName = project.name;
1520 |                                 break;
1521 |                             }
1522 |                         }
1523 |                     }
1524 |                     // Format related entities
1525 |                     const relatedText = relatedEntities.map((e) => `- **${e.name}** (${e.entityType})`).join("\n") || "Not specifically linked to any entities";
1526 |                     contextMessage = `# Memo Context: ${entityName}
1527 | 
1528 | ## Memo Details
1529 | - **Topic**: ${topic}
1530 | - **Date**: ${date}
1531 | - **Project**: ${projectName}
1532 | 
1533 | ## Content
1534 | ${content || "No content available"}
1535 | 
1536 | ## Related Entities
1537 | ${relatedText}`;
1538 |                 }
1539 |                 else if (entityType === "researchQuestion") {
1540 |                     // Find which project this research question belongs to
1541 |                     let projectName = 'Unknown project';
1542 |                     for (const relation of entityGraph.relations) {
1543 |                         if (relation.relationType === 'part_of' && relation.from === entityName) {
1544 |                             const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
1545 |                             if (project) {
1546 |                                 projectName = project.name;
1547 |                                 break;
1548 |                             }
1549 |                         }
1550 |                     }
1551 |                     // Get research question analysis
1552 |                     let analysisData;
1553 |                     try {
1554 |                         analysisData = await knowledgeGraphManager.getResearchQuestionAnalysis(projectName);
1555 |                         // Find this question in the analysis
1556 |                         const questionAnalysis = analysisData.researchQuestions?.find((q) => q.question.name === entityName);
1557 |                         if (questionAnalysis) {
1558 |                             // Format findings
1559 |                             const findingsText = questionAnalysis.findings?.map((finding) => {
1560 |                                 const status = finding.observations.find(o => o.startsWith("status:"))?.substring(7) || "preliminary";
1561 |                                 const description = finding.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1562 |                                 return `- **${finding.name}** (${status}): ${description || "No description"}`;
1563 |                             }).join("\n") || "No findings recorded yet";
1564 |                             // Format themes
1565 |                             const themesText = questionAnalysis.themes?.map((theme) => {
1566 |                                 const description = theme.observations.find(o => !o.startsWith("status:") && !o.startsWith("created:"));
1567 |                                 return `- **${theme.name}**: ${description || "No description"}`;
1568 |                             }).join("\n") || "No themes associated with this question";
1569 |                             // Format quotes
1570 |                             const quotesText = questionAnalysis.quotes?.map((quote) => {
1571 |                                 const source = quote.observations.find(o => o.startsWith("source:"))?.substring(7) || "Unknown source";
1572 |                                 const text = quote.observations.find(o => !o.startsWith("source:") && !o.startsWith("context:"));
1573 |                                 return `- "${text || "No text"}" (Source: ${source})`;
1574 |                             }).slice(0, 5).join("\n") || "No direct quotes addressing this question";
1575 |                             contextMessage = `# Research Question Context: ${entityName}
1576 | 
1577 | ## Question
1578 | ${entity.observations.find(o => !o.startsWith("created:")) || entityName}
1579 | 
1580 | ## Project
1581 | ${projectName}
1582 | 
1583 | ## Findings
1584 | ${findingsText}
1585 | 
1586 | ## Related Themes
1587 | ${themesText}
1588 | 
1589 | ## Supporting Quotes
1590 | ${quotesText}`;
1591 |                         }
1592 |                         else {
1593 |                             contextMessage = `# Research Question Context: ${entityName}
1594 | 
1595 | ## Question
1596 | ${entity.observations.find(o => !o.startsWith("created:")) || entityName}
1597 | 
1598 | ## Project
1599 | ${projectName}
1600 | 
1601 | No analysis data available for this research question.`;
1602 |                         }
1603 |                     }
1604 |                     catch (error) {
1605 |                         contextMessage = `# Research Question Context: ${entityName}
1606 | 
1607 | ## Question
1608 | ${entity.observations.find(o => !o.startsWith("created:")) || entityName}
1609 | 
1610 | ## Project
1611 | ${projectName}
1612 | 
1613 | No analysis data available for this research question.`;
1614 |                     }
1615 |                 }
1616 |                 else {
1617 |                     // Generic entity context for other entity types
1618 |                     // Get related entities
1619 |                     const relatedEntitiesData = await knowledgeGraphManager.getRelatedEntities(entityName);
1620 |                     // Format observations
1621 |                     const observationsText = entity.observations.map((obs) => `- ${obs}`).join("\n") || "No observations";
1622 |                     // Format related entities
1623 |                     const relatedText = Object.entries(relatedEntitiesData.related || {}).map(([relation, entities]) => {
1624 |                         const entitiesList = entities.map(e => `- **${e.name}** (${e.entityType})`).join("\n");
1625 |                         return `### ${relation} (${entities.length})\n${entitiesList}`;
1626 |                     }).join("\n\n") || "No related entities found";
1627 |                     contextMessage = `# Entity Context: ${entityName} (${entityType})
1628 | 
1629 | ## Observations
1630 | ${observationsText}
1631 | 
1632 | ## Related Entities
1633 | ${relatedText}`;
1634 |                 }
1635 |                 return {
1636 |                     content: [{
1637 |                             type: "text",
1638 |                             text: contextMessage
1639 |                         }]
1640 |                 };
1641 |             }
1642 |             catch (error) {
1643 |                 return {
1644 |                     content: [{
1645 |                             type: "text",
1646 |                             text: JSON.stringify({
1647 |                                 success: false,
1648 |                                 error: error instanceof Error ? error.message : String(error)
1649 |                             }, null, 2)
1650 |                         }]
1651 |                 };
1652 |             }
1653 |         });
1654 |         // Helper function to process each stage of endsession
1655 |         async function processStage(params, previousStages) {
1656 |             // Process based on the stage
1657 |             switch (params.stage) {
1658 |                 case "summary":
1659 |                     // Process summary stage
1660 |                     return {
1661 |                         stage: "summary",
1662 |                         stageNumber: params.stageNumber,
1663 |                         analysis: params.analysis || "",
1664 |                         stageData: params.stageData || {
1665 |                             summary: "",
1666 |                             duration: "",
1667 |                             project: ""
1668 |                         },
1669 |                         completed: !params.nextStageNeeded
1670 |                     };
1671 |                 case "interviewData":
1672 |                     // Process interview data stage
1673 |                     return {
1674 |                         stage: "interviewData",
1675 |                         stageNumber: params.stageNumber,
1676 |                         analysis: params.analysis || "",
1677 |                         stageData: params.stageData || { interviews: [] },
1678 |                         completed: !params.nextStageNeeded
1679 |                     };
1680 |                 case "memos":
1681 |                     // Process memos stage
1682 |                     return {
1683 |                         stage: "memos",
1684 |                         stageNumber: params.stageNumber,
1685 |                         analysis: params.analysis || "",
1686 |                         stageData: params.stageData || { memos: [] },
1687 |                         completed: !params.nextStageNeeded
1688 |                     };
1689 |                 case "codingActivity":
1690 |                     // Process coding activity stage
1691 |                     return {
1692 |                         stage: "codingActivity",
1693 |                         stageNumber: params.stageNumber,
1694 |                         analysis: params.analysis || "",
1695 |                         stageData: params.stageData || { codes: [] },
1696 |                         completed: !params.nextStageNeeded
1697 |                     };
1698 |                 case "themes":
1699 |                     // Process themes stage
1700 |                     return {
1701 |                         stage: "themes",
1702 |                         stageNumber: params.stageNumber,
1703 |                         analysis: params.analysis || "",
1704 |                         stageData: params.stageData || { themes: [] },
1705 |                         completed: !params.nextStageNeeded
1706 |                     };
1707 |                 case "projectStatus":
1708 |                     // Process project status stage
1709 |                     return {
1710 |                         stage: "projectStatus",
1711 |                         stageNumber: params.stageNumber,
1712 |                         analysis: params.analysis || "",
1713 |                         stageData: params.stageData || {
1714 |                             projectStatus: "",
1715 |                             projectObservation: ""
1716 |                         },
1717 |                         completed: !params.nextStageNeeded
1718 |                     };
1719 |                 case "assembly":
1720 |                     // Final assembly stage - compile all arguments for end-session
1721 |                     return {
1722 |                         stage: "assembly",
1723 |                         stageNumber: params.stageNumber,
1724 |                         analysis: "Final assembly of end-session arguments",
1725 |                         stageData: assembleEndSessionArgs(previousStages),
1726 |                         completed: true
1727 |                     };
1728 |                 default:
1729 |                     throw new Error(`Unknown stage: ${params.stage}`);
1730 |             }
1731 |         }
1732 |         // Helper function to assemble the final end-session arguments
1733 |         function assembleEndSessionArgs(stages) {
1734 |             const summaryStage = stages.find(s => s.stage === "summary");
1735 |             const interviewDataStage = stages.find(s => s.stage === "interviewData");
1736 |             const memosStage = stages.find(s => s.stage === "memos");
1737 |             const codingActivityStage = stages.find(s => s.stage === "codingActivity");
1738 |             const themesStage = stages.find(s => s.stage === "themes");
1739 |             const projectStatusStage = stages.find(s => s.stage === "projectStatus");
1740 |             return {
1741 |                 summary: summaryStage?.stageData?.summary || "",
1742 |                 duration: summaryStage?.stageData?.duration || "unknown",
1743 |                 project: summaryStage?.stageData?.project || "",
1744 |                 interviewData: JSON.stringify(interviewDataStage?.stageData?.interviews || []),
1745 |                 newMemos: JSON.stringify(memosStage?.stageData?.memos || []),
1746 |                 codingActivity: JSON.stringify(codingActivityStage?.stageData?.codes || []),
1747 |                 newThemes: JSON.stringify(themesStage?.stageData?.themes || []),
1748 |                 projectStatus: projectStatusStage?.stageData?.projectStatus || "",
1749 |                 projectObservation: projectStatusStage?.stageData?.projectObservation || ""
1750 |             };
1751 |         }
1752 |         /**
1753 |          * End session by processing all stages and recording the final results.
1754 |          * Only use this tool if the user asks for it.
1755 |          *
1756 |          * Usage examples:
1757 |          *
1758 |          * 1. Starting the end session process with the summary stage:
1759 |          * {
1760 |          *   "sessionId": "qual_1234567890_abc123",  // From startsession
1761 |          *   "stage": "summary",
1762 |          *   "stageNumber": 1,
1763 |          *   "totalStages": 6,
1764 |          *   "analysis": "Analyzed progress on the interview data coding",
1765 |          *   "stageData": {
1766 |          *     "summary": "Completed initial coding of participant interviews",
1767 |          *     "duration": "3 hours",
1768 |          *     "project": "Health Behavior Study"  // Project name
1769 |          *   },
1770 |          *   "nextStageNeeded": true,  // More stages coming
1771 |          *   "isRevision": false
1772 |          * }
1773 |          *
1774 |          * 2. Middle stage for themes:
1775 |          * {
1776 |          *   "sessionId": "qual_1234567890_abc123",
1777 |          *   "stage": "themes",
1778 |          *   "stageNumber": 2,
1779 |          *   "totalStages": 6,
1780 |          *   "analysis": "Identified emerging themes",
1781 |          *   "stageData": {
1782 |          *     "themes": [
1783 |          *       { "name": "Perceived Barriers", "codes": ["time_constraints", "financial_concerns"], "description": "Factors preventing healthy behaviors" },
1784 |          *       { "name": "Social Support", "codes": ["family_influence", "peer_encouragement"], "description": "External motivation from relationships" }
1785 |          *     ]
1786 |          *   },
1787 |          *   "nextStageNeeded": true,
1788 |          *   "isRevision": false
1789 |          * }
1790 |          *
1791 |          * 3. Final assembly stage:
1792 |          * {
1793 |          *   "sessionId": "qual_1234567890_abc123",
1794 |          *   "stage": "assembly",
1795 |          *   "stageNumber": 6,
1796 |          *   "totalStages": 6,
1797 |          *   "nextStageNeeded": false,  // This completes the session
1798 |          *   "isRevision": false
1799 |          * }
1800 |          */
1801 |         server.tool("endsession", toolDescriptions["endsession"], {
1802 |             sessionId: z.string().describe("The unique session identifier obtained from startsession"),
1803 |             stage: z.string().describe("Current stage of analysis: 'summary', 'themes', 'codes', 'memos', 'participantInsights', or 'assembly'"),
1804 |             stageNumber: z.number().int().positive().describe("The sequence number of the current stage (starts at 1)"),
1805 |             totalStages: z.number().int().positive().describe("Total number of stages in the workflow (typically 6 for standard workflow)"),
1806 |             analysis: z.string().optional().describe("Text analysis or observations for the current stage"),
1807 |             stageData: z.record(z.string(), z.any()).optional().describe(`Stage-specific data structure - format depends on the stage type:
1808 |         - For 'summary' stage: { summary: "Session summary text", duration: "3 hours", project: "Project Name" }
1809 |         - For 'themes' stage: { themes: [{ name: "Theme1", codes: ["code1", "code2"], description: "Theme description" }] }
1810 |         - For 'codes' stage: { codes: [{ name: "Code1", description: "Code meaning", quotes: ["Quote text"] }] }
1811 |         - For 'memos' stage: { memos: [{ title: "Memo title", content: "Detailed memo text", tags: ["tag1", "tag2"] }] }
1812 |         - For 'participantInsights' stage: { insights: [{ participant: "P1", observation: "Key insight about participant" }] }
1813 |         - For 'assembly' stage: no stageData needed - automatic assembly of previous stages`),
1814 |             nextStageNeeded: z.boolean().describe("Whether additional stages are needed after this one (false for final stage)"),
1815 |             isRevision: z.boolean().optional().describe("Whether this is revising a previous stage"),
1816 |             revisesStage: z.number().int().positive().optional().describe("If revising, which stage number is being revised")
1817 |         }, async (params) => {
1818 |             try {
1819 |                 // Load session states from persistent storage
1820 |                 const sessionStates = await loadSessionStates();
1821 |                 // Validate session ID
1822 |                 if (!sessionStates.has(params.sessionId)) {
1823 |                     return {
1824 |                         content: [{
1825 |                                 type: "text",
1826 |                                 text: JSON.stringify({
1827 |                                     success: false,
1828 |                                     error: `Session with ID ${params.sessionId} not found. Please start a new session with startsession.`
1829 |                                 }, null, 2)
1830 |                             }]
1831 |                     };
1832 |                 }
1833 |                 // Get or initialize session state
1834 |                 let sessionState = sessionStates.get(params.sessionId) || [];
1835 |                 // Process the current stage
1836 |                 const stageResult = await processStage(params, sessionState);
1837 |                 // Store updated state
1838 |                 if (params.isRevision && params.revisesStage) {
1839 |                     // Find the analysis stages in the session state
1840 |                     const analysisStages = sessionState.filter(item => item.type === 'analysis_stage') || [];
1841 |                     if (params.revisesStage <= analysisStages.length) {
1842 |                         // Replace the revised stage
1843 |                         analysisStages[params.revisesStage - 1] = {
1844 |                             type: 'analysis_stage',
1845 |                             ...stageResult
1846 |                         };
1847 |                     }
1848 |                     else {
1849 |                         // Add as a new stage
1850 |                         analysisStages.push({
1851 |                             type: 'analysis_stage',
1852 |                             ...stageResult
1853 |                         });
1854 |                     }
1855 |                     // Update the session state with the modified analysis stages
1856 |                     sessionState = [
1857 |                         ...sessionState.filter(item => item.type !== 'analysis_stage'),
1858 |                         ...analysisStages
1859 |                     ];
1860 |                 }
1861 |                 else {
1862 |                     // Add new stage
1863 |                     sessionState.push({
1864 |                         type: 'analysis_stage',
1865 |                         ...stageResult
1866 |                     });
1867 |                 }
1868 |                 // Update in persistent storage
1869 |                 sessionStates.set(params.sessionId, sessionState);
1870 |                 await saveSessionStates(sessionStates);
1871 |                 // Check if this is the final assembly stage and no more stages are needed
1872 |                 if (params.stage === "assembly" && !params.nextStageNeeded) {
1873 |                     // Get the assembled arguments
1874 |                     const args = stageResult.stageData;
1875 |                     try {
1876 |                         // Parse arguments
1877 |                         const summary = args.summary;
1878 |                         const duration = args.duration;
1879 |                         const project = args.project;
1880 |                         const interviewData = args.interviewData ? JSON.parse(args.interviewData) : [];
1881 |                         const newMemos = args.newMemos ? JSON.parse(args.newMemos) : [];
1882 |                         const codingActivity = args.codingActivity ? JSON.parse(args.codingActivity) : [];
1883 |                         const newThemes = args.newThemes ? JSON.parse(args.newThemes) : [];
1884 |                         const projectStatus = args.projectStatus;
1885 |                         const projectObservation = args.projectObservation;
1886 |                         // Update project status using the relation-based approach
1887 |                         try {
1888 |                             // Set the project status using our helper method
1889 |                             if (projectStatus) {
1890 |                                 await knowledgeGraphManager.setEntityStatus(project, projectStatus);
1891 |                             }
1892 |                             // Add observation if provided
1893 |                             if (projectObservation) {
1894 |                                 await knowledgeGraphManager.addObservations([{
1895 |                                         entityName: project,
1896 |                                         contents: [projectObservation]
1897 |                                     }]);
1898 |                             }
1899 |                         }
1900 |                         catch (error) {
1901 |                             console.error(`Error updating status for project ${project}:`, error);
1902 |                         }
1903 |                         // Record session completion in persistent storage
1904 |                         sessionState.push({
1905 |                             type: 'session_completed',
1906 |                             timestamp: new Date().toISOString(),
1907 |                             project
1908 |                         });
1909 |                         sessionStates.set(params.sessionId, sessionState);
1910 |                         await saveSessionStates(sessionStates);
1911 |                         // Prepare the summary message
1912 |                         const summaryMessage = `# Qualitative Research Session Recorded
1913 | 
1914 | I've recorded your research session focusing on the ${project} project.
1915 | 
1916 | ## Session Summary
1917 | ${summary}
1918 | 
1919 | ${interviewData.length > 0 ? `## Interviews Conducted
1920 | ${interviewData.map((i) => `- Interview with ${i.participant}`).join('\n')}` : "No interviews were recorded."}
1921 | 
1922 | ${newMemos.length > 0 ? `## Research Memos Created
1923 | ${newMemos.map((m) => `- ${m.topic}`).join('\n')}` : "No memos were created."}
1924 | 
1925 | ${codingActivity.length > 0 ? `## Coding Activity
1926 | ${codingActivity.map((c) => `- Coded ${c.dataItem} with "${c.code}"${c.note ? `: ${c.note}` : ''}`).join('\n')}` : "No coding was performed."}
1927 | 
1928 | ${newThemes.length > 0 ? `## Themes Identified
1929 | ${newThemes.map((t) => `- ${t.name}: ${t.description}`).join('\n')}` : "No themes were identified."}
1930 | 
1931 | ## Project Status
1932 | Project ${project} has been updated to: ${projectStatus}
1933 | 
1934 | Would you like me to perform any additional updates to your qualitative research knowledge graph?`;
1935 |                         // Return the final result with the session recorded message
1936 |                         return {
1937 |                             content: [{
1938 |                                     type: "text",
1939 |                                     text: JSON.stringify({
1940 |                                         success: true,
1941 |                                         stageCompleted: params.stage,
1942 |                                         nextStageNeeded: false,
1943 |                                         stageResult: stageResult,
1944 |                                         sessionRecorded: true,
1945 |                                         summaryMessage: summaryMessage
1946 |                                     }, null, 2)
1947 |                                 }]
1948 |                         };
1949 |                     }
1950 |                     catch (error) {
1951 |                         return {
1952 |                             content: [{
1953 |                                     type: "text",
1954 |                                     text: JSON.stringify({
1955 |                                         success: false,
1956 |                                         error: `Error assembling end-session arguments: ${error instanceof Error ? error.message : String(error)}`
1957 |                                     }, null, 2)
1958 |                                 }]
1959 |                         };
1960 |                     }
1961 |                 }
1962 |                 else {
1963 |                     // This is not the final stage or more stages are needed
1964 |                     // Return intermediate result
1965 |                     return {
1966 |                         content: [{
1967 |                                 type: "text",
1968 |                                 text: JSON.stringify({
1969 |                                     success: true,
1970 |                                     stageCompleted: params.stage,
1971 |                                     nextStageNeeded: params.nextStageNeeded,
1972 |                                     stageResult: stageResult,
1973 |                                     endSessionArgs: params.stage === "assembly" ? stageResult.stageData : null
1974 |                                 }, null, 2)
1975 |                             }]
1976 |                     };
1977 |                 }
1978 |             }
1979 |             catch (error) {
1980 |                 return {
1981 |                     content: [{
1982 |                             type: "text",
1983 |                             text: JSON.stringify({
1984 |                                 success: false,
1985 |                                 error: `Error recording qualitative research session: ${error instanceof Error ? error.message : String(error)}`
1986 |                             }, null, 2)
1987 |                         }]
1988 |                 };
1989 |             }
1990 |         });
1991 |         /**
1992 |          * Start a new session for qualitative research. Returns session ID, recent sessions, active projects, sample participants, top codes, and recent memos.
1993 |          * The output allows the user to easily choose what to focus on and which specific context to load.
1994 |          */
1995 |         server.tool("startsession", toolDescriptions["startsession"], {}, async () => {
1996 |             try {
1997 |                 // Generate a unique session ID
1998 |                 const sessionId = generateSessionId();
1999 |                 // Get recent sessions from persistent storage
2000 |                 const sessionStates = await loadSessionStates();
2001 |                 // Initialize the session state
2002 |                 sessionStates.set(sessionId, []);
2003 |                 await saveSessionStates(sessionStates);
2004 |                 // Convert sessions map to array and retrieve the most recent sessions
2005 |                 const recentSessions = Array.from(sessionStates.entries())
2006 |                     .map(([id, stages]) => {
2007 |                     // Extract summary data from the first stage (if it exists)
2008 |                     const summaryStage = stages.find(s => s.stage === "summary");
2009 |                     return {
2010 |                         id,
2011 |                         project: summaryStage?.stageData?.project || "Unknown project",
2012 |                         summary: summaryStage?.stageData?.summary || "No summary available"
2013 |                     };
2014 |                 })
2015 |                     .slice(0, 3); // Default to showing 3 recent sessions
2016 |                 // Query for all research projects and filter by status
2017 |                 const projectsQuery = await knowledgeGraphManager.searchNodes("entityType:project");
2018 |                 const projects = [];
2019 |                 // Filter for active projects based on has_status relation
2020 |                 for (const project of projectsQuery.entities) {
2021 |                     const status = await knowledgeGraphManager.getEntityStatus(project.name);
2022 |                     if (status === "active" || status === "in_progress" || status === "data_collection" || status === "analysis") {
2023 |                         projects.push(project);
2024 |                     }
2025 |                 }
2026 |                 // Query for a sample of participants
2027 |                 const participantsQuery = await knowledgeGraphManager.searchNodes("entityType:participant");
2028 |                 const participants = participantsQuery.entities.slice(0, 5); // Limit to 5 participants for initial display
2029 |                 // Get all codes
2030 |                 const codesQuery = await knowledgeGraphManager.searchNodes("entityType:code");
2031 |                 const codes = codesQuery.entities.slice(0, 10); // Top 10 codes
2032 |                 // Get recent memos
2033 |                 const memosQuery = await knowledgeGraphManager.searchNodes("entityType:memo");
2034 |                 const memos = memosQuery.entities.slice(0, 3); // Most recent 3 memos
2035 |                 // Format the context information using entity-relation approach
2036 |                 const projectsText = await Promise.all(projects.map(async (p) => {
2037 |                     const status = await knowledgeGraphManager.getEntityStatus(p.name) || "Unknown";
2038 |                     const priority = await knowledgeGraphManager.getEntityPriority(p.name);
2039 |                     const priorityText = priority ? `, Priority: ${priority}` : "";
2040 |                     // Show truncated preview of first observation
2041 |                     const preview = p.observations.length > 0
2042 |                         ? `${p.observations[0].substring(0, 60)}${p.observations[0].length > 60 ? '...' : ''}`
2043 |                         : "No description";
2044 |                     return `- **${p.name}** (Status: ${status}${priorityText}): ${preview}`;
2045 |                 }));
2046 |                 const participantsText = await Promise.all(participants.map(async (p) => {
2047 |                     const status = await knowledgeGraphManager.getEntityStatus(p.name) || "Active";
2048 |                     // Show truncated preview of first observation for demographics
2049 |                     const preview = p.observations.length > 0
2050 |                         ? `${p.observations[0].substring(0, 60)}${p.observations[0].length > 60 ? '...' : ''}`
2051 |                         : "No demographics";
2052 |                     return `- **${p.name}** (Status: ${status}): ${preview}`;
2053 |                 }));
2054 |                 const codesText = await Promise.all(codes.map(async (c) => {
2055 |                     const status = await knowledgeGraphManager.getEntityStatus(c.name) || "initial";
2056 |                     // Show truncated preview of first observation for description
2057 |                     const preview = c.observations.length > 0
2058 |                         ? `${c.observations[0].substring(0, 60)}${c.observations[0].length > 60 ? '...' : ''}`
2059 |                         : "No description";
2060 |                     return `- **${c.name}** (Status: ${status}): ${preview}`;
2061 |                 }));
2062 |                 const memosText = await Promise.all(memos.map(async (m) => {
2063 |                     const status = await knowledgeGraphManager.getEntityStatus(m.name) || "draft";
2064 |                     // Show truncated preview of first observation for content
2065 |                     const preview = m.observations.length > 0
2066 |                         ? `${m.observations[0].substring(0, 60)}${m.observations[0].length > 60 ? '...' : ''}`
2067 |                         : "No content";
2068 |                     return `- **${m.name}** (Status: ${status}): ${preview}`;
2069 |                 }));
2070 |                 const sessionsText = recentSessions.map(s => {
2071 |                     return `- ${s.project} - ${s.summary.substring(0, 60)}${s.summary.length > 60 ? '...' : ''}`;
2072 |                 }).join("\n");
2073 |                 return {
2074 |                     content: [{
2075 |                             type: "text",
2076 |                             text: `# Choose what to focus on in this session
2077 | 
2078 | ## Session ID
2079 | \`${sessionId}\`
2080 | 
2081 | ## Recent Research Sessions
2082 | ${sessionsText || "No recent sessions found."}
2083 | 
2084 | ## Active Research Projects
2085 | ${projectsText.join("\n") || "No active projects found."}
2086 | 
2087 | ## Sample Participants
2088 | ${participantsText.join("\n") || "No participants found."}
2089 | 
2090 | ## Top Codes
2091 | ${codesText.join("\n") || "No codes found."}
2092 | 
2093 | ## Recent Memos
2094 | ${memosText.join("\n") || "No memos found."}
2095 | 
2096 | To load specific context, use the \`loadcontext\` tool with the entity name and session ID - ${sessionId}`
2097 |                         }]
2098 |                 };
2099 |             }
2100 |             catch (error) {
2101 |                 return {
2102 |                     content: [{
2103 |                             type: "text",
2104 |                             text: JSON.stringify({
2105 |                                 success: false,
2106 |                                 error: error instanceof Error ? error.message : String(error)
2107 |                             }, null, 2)
2108 |                         }]
2109 |                 };
2110 |             }
2111 |         });
2112 |         /**
2113 |          * Create new entities, relations, and observations.
2114 |          */
2115 |         server.tool("buildcontext", toolDescriptions["buildcontext"], {
2116 |             type: z.enum(["entities", "relations", "observations"]).describe("Type of creation operation: 'entities', 'relations', or 'observations'"),
2117 |             data: z.array(z.any()).describe("Data for the creation operation, structure varies by type but must be an array")
2118 |         }, async ({ type, data }) => {
2119 |             try {
2120 |                 let result;
2121 |                 switch (type) {
2122 |                     case "entities":
2123 |                         // Validate entity types
2124 |                         for (const entity of data) {
2125 |                             if (!validateEntityType(entity.entityType)) {
2126 |                                 throw new Error(`Invalid entity type: ${entity.entityType}`);
2127 |                             }
2128 |                         }
2129 |                         // Ensure entities match the Entity interface
2130 |                         const typedEntities = data.map((e) => ({
2131 |                             name: e.name,
2132 |                             entityType: e.entityType,
2133 |                             observations: e.observations
2134 |                         }));
2135 |                         result = await knowledgeGraphManager.createEntities(typedEntities);
2136 |                         return {
2137 |                             content: [{
2138 |                                     type: "text",
2139 |                                     text: JSON.stringify({ success: true, created: result }, null, 2)
2140 |                                 }]
2141 |                         };
2142 |                     case "relations":
2143 |                         // Validate relation types
2144 |                         for (const relation of data) {
2145 |                             if (!validateRelationType(relation.relationType)) {
2146 |                                 throw new Error(`Invalid relation type: ${relation.relationType}`);
2147 |                             }
2148 |                         }
2149 |                         // Ensure relations match the Relation interface
2150 |                         const typedRelations = data.map((r) => ({
2151 |                             from: r.from,
2152 |                             to: r.to,
2153 |                             relationType: r.relationType
2154 |                         }));
2155 |                         result = await knowledgeGraphManager.createRelations(typedRelations);
2156 |                         return {
2157 |                             content: [{
2158 |                                     type: "text",
2159 |                                     text: JSON.stringify({ success: true, created: result }, null, 2)
2160 |                                 }]
2161 |                         };
2162 |                     case "observations":
2163 |                         // For qualitative researcher domain, addObservations takes an array
2164 |                         // Ensure observations match the required interface
2165 |                         const typedObservations = Array.isArray(data) ? data.map((o) => ({
2166 |                             entityName: o.entityName,
2167 |                             contents: Array.isArray(o.contents) ? o.contents :
2168 |                                 Array.isArray(o.observations) ? o.observations : []
2169 |                         })) : [data];
2170 |                         result = await knowledgeGraphManager.addObservations(typedObservations);
2171 |                         return {
2172 |                             content: [{
2173 |                                     type: "text",
2174 |                                     text: JSON.stringify({ success: true, added: result }, null, 2)
2175 |                                 }]
2176 |                         };
2177 |                     default:
2178 |                         throw new Error(`Invalid type: ${type}. Must be 'entities', 'relations', or 'observations'.`);
2179 |                 }
2180 |             }
2181 |             catch (error) {
2182 |                 return {
2183 |                     content: [{
2184 |                             type: "text",
2185 |                             text: JSON.stringify({
2186 |                                 success: false,
2187 |                                 error: error instanceof Error ? error.message : String(error)
2188 |                             }, null, 2)
2189 |                         }]
2190 |                 };
2191 |             }
2192 |         });
2193 |         /**
2194 |          * Delete entities, relations, or observations.
2195 |          */
2196 |         server.tool("deletecontext", toolDescriptions["deletecontext"], {
2197 |             type: z.enum(["entities", "relations", "observations"]).describe("Type of deletion operation: 'entities', 'relations', or 'observations'"),
2198 |             data: z.array(z.any()).describe("Data for the deletion operation, structure varies by type but must be an array")
2199 |         }, async ({ type, data }) => {
2200 |             try {
2201 |                 switch (type) {
2202 |                     case "entities":
2203 |                         await knowledgeGraphManager.deleteEntities(data);
2204 |                         return {
2205 |                             content: [{
2206 |                                     type: "text",
2207 |                                     text: JSON.stringify({ success: true, message: `Deleted ${data.length} entities` }, null, 2)
2208 |                                 }]
2209 |                         };
2210 |                     case "relations":
2211 |                         // Ensure relations match the Relation interface
2212 |                         const typedRelations = data.map((r) => ({
2213 |                             from: r.from,
2214 |                             to: r.to,
2215 |                             relationType: r.relationType
2216 |                         }));
2217 |                         await knowledgeGraphManager.deleteRelations(typedRelations);
2218 |                         return {
2219 |                             content: [{
2220 |                                     type: "text",
2221 |                                     text: JSON.stringify({ success: true, message: `Deleted ${data.length} relations` }, null, 2)
2222 |                                 }]
2223 |                         };
2224 |                     case "observations":
2225 |                         // Ensure deletions match the required interface
2226 |                         const typedDeletions = data.map((d) => ({
2227 |                             entityName: d.entityName,
2228 |                             observations: d.observations
2229 |                         }));
2230 |                         await knowledgeGraphManager.deleteObservations(typedDeletions);
2231 |                         return {
2232 |                             content: [{
2233 |                                     type: "text",
2234 |                                     text: JSON.stringify({ success: true, message: `Deleted observations from ${data.length} entities` }, null, 2)
2235 |                                 }]
2236 |                         };
2237 |                     default:
2238 |                         throw new Error(`Invalid type: ${type}. Must be 'entities', 'relations', or 'observations'.`);
2239 |                 }
2240 |             }
2241 |             catch (error) {
2242 |                 return {
2243 |                     content: [{
2244 |                             type: "text",
2245 |                             text: JSON.stringify({
2246 |                                 success: false,
2247 |                                 error: error instanceof Error ? error.message : String(error)
2248 |                             }, null, 2)
2249 |                         }]
2250 |                 };
2251 |             }
2252 |         });
2253 |         /**
2254 |          * Get information about the graph, search for nodes, open nodes, get project overview, get participant profile, get codes, get themes, get transcript, get memo, get analysis, get codebook, or get related entities.
2255 |          */
2256 |         server.tool("advancedcontext", toolDescriptions["advancedcontext"], {
2257 |             type: z.enum([
2258 |                 "graph",
2259 |                 "search",
2260 |                 "nodes",
2261 |                 "project",
2262 |                 "participant",
2263 |                 "codes",
2264 |                 "themes",
2265 |                 "transcript",
2266 |                 "memo",
2267 |                 "analysis",
2268 |                 "codebook",
2269 |                 "related"
2270 |             ]).describe("Type of get operation"),
2271 |             params: z.record(z.string(), z.any()).describe("Parameters for the get operation, structure varies by type")
2272 |         }, async ({ type, params }) => {
2273 |             try {
2274 |                 let result;
2275 |                 switch (type) {
2276 |                     case "graph":
2277 |                         result = await knowledgeGraphManager.readGraph();
2278 |                         return {
2279 |                             content: [{
2280 |                                     type: "text",
2281 |                                     text: JSON.stringify({ success: true, graph: result }, null, 2)
2282 |                                 }]
2283 |                         };
2284 |                     case "search":
2285 |                         result = await knowledgeGraphManager.searchNodes(params.query);
2286 |                         return {
2287 |                             content: [{
2288 |                                     type: "text",
2289 |                                     text: JSON.stringify({ success: true, results: result }, null, 2)
2290 |                                 }]
2291 |                         };
2292 |                     case "nodes":
2293 |                         result = await knowledgeGraphManager.openNodes(params.names);
2294 |                         return {
2295 |                             content: [{
2296 |                                     type: "text",
2297 |                                     text: JSON.stringify({ success: true, nodes: result }, null, 2)
2298 |                                 }]
2299 |                         };
2300 |                     case "project":
2301 |                         result = await knowledgeGraphManager.getProjectOverview(params.projectName);
2302 |                         return {
2303 |                             content: [{
2304 |                                     type: "text",
2305 |                                     text: JSON.stringify({ success: true, project: result }, null, 2)
2306 |                                 }]
2307 |                         };
2308 |                     case "participant":
2309 |                         result = await knowledgeGraphManager.getParticipantProfile(params.participantName);
2310 |                         return {
2311 |                             content: [{
2312 |                                     type: "text",
2313 |                                     text: JSON.stringify({ success: true, participant: result }, null, 2)
2314 |                                 }]
2315 |                         };
2316 |                     case "codes":
2317 |                         // Use searchNodes for codes instead of a specialized method
2318 |                         result = await knowledgeGraphManager.searchNodes(`entityType:code ${params.projectName ? `project:${params.projectName}` : ""}`);
2319 |                         return {
2320 |                             content: [{
2321 |                                     type: "text",
2322 |                                     text: JSON.stringify({ success: true, codes: result }, null, 2)
2323 |                                 }]
2324 |                         };
2325 |                     case "themes":
2326 |                         // Use searchNodes for themes
2327 |                         result = await knowledgeGraphManager.searchNodes(`entityType:theme ${params.projectName ? `project:${params.projectName}` : ""}`);
2328 |                         return {
2329 |                             content: [{
2330 |                                     type: "text",
2331 |                                     text: JSON.stringify({ success: true, themes: result }, null, 2)
2332 |                                 }]
2333 |                         };
2334 |                     case "transcript":
2335 |                         // Use searchNodes to find the transcript
2336 |                         const transcriptQuery = await knowledgeGraphManager.searchNodes(`entityType:transcript participant:${params.participantName} ${params.interviewId ? `interview:${params.interviewId}` : ""}`);
2337 |                         return {
2338 |                             content: [{
2339 |                                     type: "text",
2340 |                                     text: JSON.stringify({ success: true, transcript: transcriptQuery }, null, 2)
2341 |                                 }]
2342 |                         };
2343 |                     case "memo":
2344 |                         // Use openNodes to get the specific memo
2345 |                         const memoResult = await knowledgeGraphManager.openNodes([params.memoName]);
2346 |                         return {
2347 |                             content: [{
2348 |                                     type: "text",
2349 |                                     text: JSON.stringify({ success: true, memo: memoResult }, null, 2)
2350 |                                 }]
2351 |                         };
2352 |                     case "analysis":
2353 |                         // Use searchNodes to get analysis artifacts
2354 |                         const analysisQuery = await knowledgeGraphManager.searchNodes(`entityType:analysis project:${params.projectName}`);
2355 |                         return {
2356 |                             content: [{
2357 |                                     type: "text",
2358 |                                     text: JSON.stringify({ success: true, analysis: analysisQuery }, null, 2)
2359 |                                 }]
2360 |                         };
2361 |                     case "codebook":
2362 |                         // Use searchNodes to get codebook entries
2363 |                         const codebookQuery = await knowledgeGraphManager.searchNodes(`entityType:code project:${params.projectName}`);
2364 |                         return {
2365 |                             content: [{
2366 |                                     type: "text",
2367 |                                     text: JSON.stringify({ success: true, codebook: codebookQuery }, null, 2)
2368 |                                 }]
2369 |                         };
2370 |                     case "related":
2371 |                         // For the related case, we don't have a specialized method in the manager
2372 |                         // So we'll use the generic KnowledgeGraph search capabilities
2373 |                         const entityGraph = await knowledgeGraphManager.searchNodes(params.entityName);
2374 |                         const entity = entityGraph.entities.find(e => e.name === params.entityName);
2375 |                         if (!entity) {
2376 |                             throw new Error(`Entity "${params.entityName}" not found`);
2377 |                         }
2378 |                         // Find related entities
2379 |                         const relations = entityGraph.relations.filter(r => r.from === params.entityName || r.to === params.entityName);
2380 |                         const relatedNames = relations.map(r => r.from === params.entityName ? r.to : r.from);
2381 |                         if (relatedNames.length === 0) {
2382 |                             return {
2383 |                                 content: [{
2384 |                                         type: "text",
2385 |                                         text: JSON.stringify({ success: true, related: { entity, relatedEntities: [] } }, null, 2)
2386 |                                     }]
2387 |                             };
2388 |                         }
2389 |                         const relatedEntitiesGraph = await knowledgeGraphManager.openNodes(relatedNames);
2390 |                         return {
2391 |                             content: [{
2392 |                                     type: "text",
2393 |                                     text: JSON.stringify({
2394 |                                         success: true,
2395 |                                         related: {
2396 |                                             entity,
2397 |                                             relations,
2398 |                                             relatedEntities: relatedEntitiesGraph.entities
2399 |                                         }
2400 |                                     }, null, 2)
2401 |                                 }]
2402 |                         };
2403 |                     default:
2404 |                         throw new Error(`Invalid type: ${type}. Must be one of the supported get operation types.`);
2405 |                 }
2406 |             }
2407 |             catch (error) {
2408 |                 return {
2409 |                     content: [{
2410 |                             type: "text",
2411 |                             text: JSON.stringify({
2412 |                                 success: false,
2413 |                                 error: error instanceof Error ? error.message : String(error)
2414 |                             }, null, 2)
2415 |                         }]
2416 |                 };
2417 |             }
2418 |         });
2419 |         // Connect the server to the transport
2420 |         const transport = new StdioServerTransport();
2421 |         await server.connect(transport);
2422 |     }
2423 |     catch (error) {
2424 |         console.error("Error starting server:", error);
2425 |         process.exit(1);
2426 |     }
2427 | }
2428 | // Run the main function
2429 | main().catch(error => {
2430 |     console.error("Unhandled error:", error);
2431 |     process.exit(1);
2432 | });
2433 | // Export the KnowledgeGraphManager for testing
2434 | export { KnowledgeGraphManager };
2435 | 
```
Page 9/13FirstPrevNextLast