This is page 11 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
--------------------------------------------------------------------------------
/project/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | // Updated imports using the modern MCP SDK API
4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6 | import { z } from "zod";
7 |
8 | // Node.js type declarations
9 | import { promises as fs } from 'fs';
10 | import * as path from 'path';
11 | import { fileURLToPath } from 'url';
12 | import { readFileSync, existsSync } from "fs";
13 |
14 | // Define memory file path using environment variable with fallback
15 | const parentPath = path.dirname(fileURLToPath(import.meta.url));
16 | const defaultMemoryPath = path.join(parentPath, 'memory.json');
17 | const defaultSessionsPath = path.join(parentPath, 'sessions.json');
18 |
19 | // Properly handle absolute and relative paths for MEMORY_FILE_PATH
20 | const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH
21 | ? path.isAbsolute(process.env.MEMORY_FILE_PATH)
22 | ? process.env.MEMORY_FILE_PATH // Use absolute path as is
23 | : path.join(process.cwd(), process.env.MEMORY_FILE_PATH) // Relative to current working directory
24 | : defaultMemoryPath; // Default fallback
25 |
26 | // Properly handle absolute and relative paths for SESSIONS_FILE_PATH
27 | const SESSIONS_FILE_PATH = process.env.SESSIONS_FILE_PATH
28 | ? path.isAbsolute(process.env.SESSIONS_FILE_PATH)
29 | ? process.env.SESSIONS_FILE_PATH // Use absolute path as is
30 | : path.join(process.cwd(), process.env.SESSIONS_FILE_PATH) // Relative to current working directory
31 | : defaultSessionsPath; // Default fallback
32 |
33 | // Project management specific entity types
34 | const validEntityTypes = [
35 | 'project', // The main container for all related entities
36 | 'task', // Individual work items that need to be completed
37 | 'milestone', // Key checkpoints or deliverables in the project
38 | 'resource', // Materials, tools, or assets needed for the project
39 | 'teamMember', // People involved in the project
40 | 'note', // Documentation, ideas, or observations
41 | 'document', // Formal project documents
42 | 'issue', // Problems or blockers
43 | 'risk', // Potential future problems
44 | 'decision', // Important choices made during the project
45 | 'dependency', // External requirements or prerequisites
46 | 'component', // Parts or modules of the project
47 | 'stakeholder', // People affected by or interested in the project
48 | 'change', // Modifications to project scope or requirements
49 | 'status', // Entity status values
50 | 'priority' // Entity priority values
51 | ] as const;
52 |
53 | // Type for entity types to ensure type safety
54 | type EntityType = typeof validEntityTypes[number];
55 |
56 | // Validation functions
57 | function isValidEntityType(type: string): type is EntityType {
58 | return validEntityTypes.includes(type as any);
59 | }
60 |
61 | function validateEntityType(type: string): void {
62 | if (!isValidEntityType(type)) {
63 | throw new Error(`Invalid entity type: ${type}. Valid types are: ${validEntityTypes.join(', ')}`);
64 | }
65 | }
66 |
67 | const __filename = fileURLToPath(import.meta.url);
68 | const __dirname = path.dirname(__filename);
69 |
70 | // Collect tool descriptions from text files
71 | const toolDescriptions: Record<string, string> = {
72 | 'startsession': '',
73 | 'loadcontext': '',
74 | 'deletecontext': '',
75 | 'buildcontext': '',
76 | 'advancedcontext': '',
77 | 'endsession': '',
78 | };
79 | for (const tool of Object.keys(toolDescriptions)) {
80 | const descriptionFilePath = path.resolve(
81 | __dirname,
82 | `project_${tool}.txt`
83 | );
84 | if (existsSync(descriptionFilePath)) {
85 | toolDescriptions[tool] = readFileSync(descriptionFilePath, 'utf-8');
86 | }
87 | }
88 |
89 | // Session management functions
90 | async function loadSessionStates(): Promise<Map<string, any[]>> {
91 | try {
92 | const fileContent = await fs.readFile(SESSIONS_FILE_PATH, 'utf-8');
93 | const sessions = JSON.parse(fileContent);
94 | // Convert from object to Map
95 | const sessionsMap = new Map<string, any[]>();
96 | for (const [key, value] of Object.entries(sessions)) {
97 | sessionsMap.set(key, value as any[]);
98 | }
99 | return sessionsMap;
100 | } catch (error) {
101 | if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") {
102 | return new Map<string, any[]>();
103 | }
104 | throw error;
105 | }
106 | }
107 |
108 | async function saveSessionStates(sessionsMap: Map<string, any[]>): Promise<void> {
109 | // Convert from Map to object
110 | const sessions: Record<string, any[]> = {};
111 | for (const [key, value] of sessionsMap.entries()) {
112 | sessions[key] = value;
113 | }
114 | await fs.writeFile(SESSIONS_FILE_PATH, JSON.stringify(sessions, null, 2), 'utf-8');
115 | }
116 |
117 | // Generate a unique session ID
118 | function generateSessionId(): string {
119 | return `proj_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
120 | }
121 |
122 | // Define common relation types for project entities
123 | const VALID_RELATION_TYPES = [
124 | 'part_of', // Indicates an entity is a component/subset of another
125 | 'depends_on', // Shows dependencies between entities
126 | 'assigned_to', // Links tasks to team members
127 | 'created_by', // Tracks who created an entity
128 | 'modified_by', // Records who changed an entity
129 | 'related_to', // Shows general connections between entities
130 | 'blocks', // Indicates one entity is blocking another
131 | 'manages', // Shows management relationships
132 | 'contributes_to', // Shows contributions to entities
133 | 'documents', // Links documentation to entities
134 | 'scheduled_for', // Connects entities to dates or timeframes
135 | 'responsible_for', // Assigns ownership/responsibility
136 | 'reports_to', // Indicates reporting relationships
137 | 'categorized_as', // Links entities to categories or types
138 | 'required_for', // Shows requirements for completion
139 | 'discovered_in', // Links issues to their discovery context
140 | 'resolved_by', // Shows what resolved an issue
141 | 'impacted_by', // Shows impact relationships
142 | 'stakeholder_of', // Links stakeholders to projects/components
143 | 'prioritized_as', // Indicates priority levels
144 | 'has_status', // Connects an entity to its status
145 | 'has_priority', // Connects an entity to its priority
146 | 'precedes' // Indicates one entity comes before another in sequence
147 | ];
148 |
149 | // Valid status and priority values
150 | const VALID_STATUS_VALUES = ['active', 'completed', 'pending', 'blocked', 'cancelled'];
151 | const VALID_PRIORITY_VALUES = ['high', 'low'];
152 |
153 | // Status values for different entity types
154 | const STATUS_VALUES = {
155 | project: ['planning', 'in_progress', 'on_hold', 'completed', 'cancelled', 'archived'],
156 | task: ['not_started', 'in_progress', 'blocked', 'under_review', 'completed', 'cancelled'],
157 | milestone: ['planned', 'approaching', 'reached', 'missed', 'rescheduled'],
158 | issue: ['identified', 'analyzing', 'fixing', 'testing', 'resolved', 'wont_fix'],
159 | risk: ['identified', 'monitoring', 'mitigating', 'occurred', 'avoided', 'accepted'],
160 | decision: ['proposed', 'under_review', 'approved', 'rejected', 'implemented', 'reversed']
161 | };
162 |
163 | // We are storing our memory using entities, relations, and observations in a graph structure
164 | interface Entity {
165 | name: string;
166 | entityType: EntityType;
167 | observations: string[];
168 | embedding?: Embedding;
169 | }
170 |
171 | interface Relation {
172 | from: string;
173 | to: string;
174 | relationType: string;
175 | observations?: string[];
176 | }
177 |
178 | interface KnowledgeGraph {
179 | entities: Entity[];
180 | relations: Relation[];
181 | }
182 |
183 | // Add the Embedding type definition near the top of the file
184 | type Embedding = number[];
185 |
186 | class KnowledgeGraphManager {
187 | public async loadGraph(): Promise<KnowledgeGraph> {
188 | try {
189 | const fileContent = await fs.readFile(MEMORY_FILE_PATH, 'utf-8');
190 | return JSON.parse(fileContent);
191 | } catch (error) {
192 | // If file doesn't exist or is invalid, return an empty graph
193 | return { entities: [], relations: [] };
194 | }
195 | }
196 |
197 | private async saveGraph(graph: KnowledgeGraph): Promise<void> {
198 | await fs.writeFile(MEMORY_FILE_PATH, JSON.stringify(graph, null, 2), 'utf-8');
199 | }
200 |
201 | // Initialize status and priority entities
202 | async initializeStatusAndPriority(): Promise<void> {
203 | const graph = await this.loadGraph();
204 |
205 | // Create status entities if they don't exist
206 | for (const statusValue of VALID_STATUS_VALUES) {
207 | const statusName = `status:${statusValue}`;
208 | if (!graph.entities.some(e => e.name === statusName && e.entityType === 'status')) {
209 | graph.entities.push({
210 | name: statusName,
211 | entityType: 'status',
212 | observations: [`A ${statusValue} status value`]
213 | });
214 | }
215 | }
216 |
217 | // Create priority entities if they don't exist
218 | for (const priorityValue of VALID_PRIORITY_VALUES) {
219 | const priorityName = `priority:${priorityValue}`;
220 | if (!graph.entities.some(e => e.name === priorityName && e.entityType === 'priority')) {
221 | graph.entities.push({
222 | name: priorityName,
223 | entityType: 'priority',
224 | observations: [`A ${priorityValue} priority value`]
225 | });
226 | }
227 | }
228 |
229 | await this.saveGraph(graph);
230 | }
231 |
232 | // Helper method to get status of an entity
233 | async getEntityStatus(entityName: string): Promise<string | null> {
234 | const graph = await this.loadGraph();
235 |
236 | // Find status relation for this entity
237 | const statusRelation = graph.relations.find(r =>
238 | r.from === entityName &&
239 | r.relationType === 'has_status'
240 | );
241 |
242 | if (statusRelation) {
243 | // Extract status value from the status entity name (status:value)
244 | return statusRelation.to.split(':')[1];
245 | }
246 |
247 | return null;
248 | }
249 |
250 | // Helper method to get priority of an entity
251 | async getEntityPriority(entityName: string): Promise<string | null> {
252 | const graph = await this.loadGraph();
253 |
254 | // Find priority relation for this entity
255 | const priorityRelation = graph.relations.find(r =>
256 | r.from === entityName &&
257 | r.relationType === 'has_priority'
258 | );
259 |
260 | if (priorityRelation) {
261 | // Extract priority value from the priority entity name (priority:value)
262 | return priorityRelation.to.split(':')[1];
263 | }
264 |
265 | return null;
266 | }
267 |
268 | // Helper method to set status of an entity
269 | async setEntityStatus(entityName: string, statusValue: string): Promise<void> {
270 | if (!VALID_STATUS_VALUES.includes(statusValue)) {
271 | throw new Error(`Invalid status value: ${statusValue}. Valid values are: ${VALID_STATUS_VALUES.join(', ')}`);
272 | }
273 |
274 | const graph = await this.loadGraph();
275 |
276 | // Remove any existing status relations for this entity
277 | graph.relations = graph.relations.filter(r =>
278 | !(r.from === entityName && r.relationType === 'has_status')
279 | );
280 |
281 | // Add new status relation
282 | graph.relations.push({
283 | from: entityName,
284 | to: `status:${statusValue}`,
285 | relationType: 'has_status'
286 | });
287 |
288 | await this.saveGraph(graph);
289 | }
290 |
291 | // Helper method to set priority of an entity
292 | async setEntityPriority(entityName: string, priorityValue: string): Promise<void> {
293 | if (!VALID_PRIORITY_VALUES.includes(priorityValue)) {
294 | throw new Error(`Invalid priority value: ${priorityValue}. Valid values are: ${VALID_PRIORITY_VALUES.join(', ')}`);
295 | }
296 |
297 | const graph = await this.loadGraph();
298 |
299 | // Remove any existing priority relations for this entity
300 | graph.relations = graph.relations.filter(r =>
301 | !(r.from === entityName && r.relationType === 'has_priority')
302 | );
303 |
304 | // Add new priority relation
305 | graph.relations.push({
306 | from: entityName,
307 | to: `priority:${priorityValue}`,
308 | relationType: 'has_priority'
309 | });
310 |
311 | await this.saveGraph(graph);
312 | }
313 |
314 | async createEntities(entities: Entity[]): Promise<KnowledgeGraph> {
315 | const graph = await this.loadGraph();
316 |
317 | // Validate entity names don't already exist
318 | for (const entity of entities) {
319 | if (graph.entities.some(e => e.name === entity.name)) {
320 | throw new Error(`Entity with name ${entity.name} already exists`);
321 | }
322 | validateEntityType(entity.entityType);
323 | }
324 |
325 | // Add new entities
326 | graph.entities.push(...entities);
327 |
328 | // Save updated graph
329 | await this.saveGraph(graph);
330 | return graph;
331 | }
332 |
333 | async createRelations(relations: Relation[]): Promise<KnowledgeGraph> {
334 | const graph = await this.loadGraph();
335 |
336 | // Validate relations
337 | for (const relation of relations) {
338 | // Check if entities exist
339 | if (!graph.entities.some(e => e.name === relation.from)) {
340 | throw new Error(`Entity '${relation.from}' not found`);
341 | }
342 | if (!graph.entities.some(e => e.name === relation.to)) {
343 | throw new Error(`Entity '${relation.to}' not found`);
344 | }
345 | if (!VALID_RELATION_TYPES.includes(relation.relationType)) {
346 | throw new Error(`Invalid relation type: ${relation.relationType}. Valid types are: ${VALID_RELATION_TYPES.join(', ')}`);
347 | }
348 |
349 | // Check if relation already exists
350 | if (graph.relations.some(r =>
351 | r.from === relation.from &&
352 | r.to === relation.to &&
353 | r.relationType === relation.relationType
354 | )) {
355 | throw new Error(`Relation from '${relation.from}' to '${relation.to}' with type '${relation.relationType}' already exists`);
356 | }
357 | }
358 |
359 | // Add relations
360 | graph.relations.push(...relations);
361 |
362 | // Save updated graph
363 | await this.saveGraph(graph);
364 | return graph;
365 | }
366 |
367 | async addObservations(entityName: string, observations: string[]): Promise<KnowledgeGraph> {
368 | const graph = await this.loadGraph();
369 |
370 | // Find the entity
371 | const entity = graph.entities.find(e => e.name === entityName);
372 | if (!entity) {
373 | throw new Error(`Entity '${entityName}' not found`);
374 | }
375 |
376 | // Add observations
377 | entity.observations.push(...observations);
378 |
379 | // Save updated graph
380 | await this.saveGraph(graph);
381 | return graph;
382 | }
383 |
384 | async deleteEntities(entityNames: string[]): Promise<void> {
385 | const graph = await this.loadGraph();
386 |
387 | // Remove the entities
388 | graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
389 |
390 | // Remove any relations that involve those entities
391 | graph.relations = graph.relations.filter(
392 | r => !entityNames.includes(r.from) && !entityNames.includes(r.to)
393 | );
394 |
395 | await this.saveGraph(graph);
396 | }
397 |
398 | async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
399 | const graph = await this.loadGraph();
400 |
401 | for (const deletion of deletions) {
402 | const entity = graph.entities.find(e => e.name === deletion.entityName);
403 | if (entity) {
404 | entity.observations = entity.observations.filter(
405 | o => !deletion.observations.includes(o)
406 | );
407 | }
408 | }
409 |
410 | await this.saveGraph(graph);
411 | }
412 |
413 | async deleteRelations(relations: Relation[]): Promise<void> {
414 | const graph = await this.loadGraph();
415 |
416 | // Remove matching relations
417 | graph.relations = graph.relations.filter(r =>
418 | !relations.some(
419 | rel => r.from === rel.from && r.to === rel.to && r.relationType === rel.relationType
420 | )
421 | );
422 |
423 | await this.saveGraph(graph);
424 | }
425 |
426 | async readGraph(): Promise<KnowledgeGraph> {
427 | return await this.loadGraph();
428 | }
429 |
430 | async searchNodes(query: string): Promise<KnowledgeGraph> {
431 | const graph = await this.loadGraph();
432 | const lowerQuery = query.toLowerCase();
433 |
434 | // Simple implementation: search entity names, types, and observations
435 | const matchingEntities = graph.entities.filter(entity =>
436 | entity.name.toLowerCase().includes(lowerQuery) ||
437 | entity.entityType.toLowerCase().includes(lowerQuery) ||
438 | entity.observations.some(o => o.toLowerCase().includes(lowerQuery))
439 | );
440 |
441 | // Get entity names for filtering relations
442 | const matchingEntityNames = new Set(matchingEntities.map(e => e.name));
443 |
444 | // Find relations between matching entities
445 | const matchingRelations = graph.relations.filter(relation =>
446 | matchingEntityNames.has(relation.from) && matchingEntityNames.has(relation.to)
447 | );
448 |
449 | // Also include relations where the relation type matches the query
450 | const additionalRelations = graph.relations.filter(relation =>
451 | relation.relationType.toLowerCase().includes(lowerQuery) ||
452 | (relation.observations && relation.observations.some(o => o.toLowerCase().includes(lowerQuery)))
453 | );
454 |
455 | // Merge relations without duplicates
456 | const allRelations = [...matchingRelations];
457 | for (const relation of additionalRelations) {
458 | if (!allRelations.some(r =>
459 | r.from === relation.from &&
460 | r.to === relation.to &&
461 | r.relationType === relation.relationType
462 | )) {
463 | allRelations.push(relation);
464 |
465 | // Add the entities involved in these additional relations
466 | if (!matchingEntityNames.has(relation.from)) {
467 | const fromEntity = graph.entities.find(e => e.name === relation.from);
468 | if (fromEntity) {
469 | matchingEntities.push(fromEntity);
470 | matchingEntityNames.add(relation.from);
471 | }
472 | }
473 |
474 | if (!matchingEntityNames.has(relation.to)) {
475 | const toEntity = graph.entities.find(e => e.name === relation.to);
476 | if (toEntity) {
477 | matchingEntities.push(toEntity);
478 | matchingEntityNames.add(relation.to);
479 | }
480 | }
481 | }
482 | }
483 |
484 | return {
485 | entities: matchingEntities,
486 | relations: allRelations
487 | };
488 | }
489 |
490 | async openNodes(names: string[]): Promise<KnowledgeGraph> {
491 | const graph = await this.loadGraph();
492 |
493 | // Find the specified entities
494 | const entities = graph.entities.filter(e => names.includes(e.name));
495 |
496 | // Find relations between the specified entities
497 | const relations = graph.relations.filter(r =>
498 | names.includes(r.from) && names.includes(r.to)
499 | );
500 |
501 | return {
502 | entities,
503 | relations
504 | };
505 | }
506 |
507 | // Provides a comprehensive view of a project including tasks, milestones, team members, issues, etc.
508 | async getProjectOverview(projectName: string): Promise<any> {
509 | const graph = await this.loadGraph();
510 |
511 | // Find the project
512 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
513 | if (!project) {
514 | throw new Error(`Project '${projectName}' not found`);
515 | }
516 |
517 | // Extract project info from observations
518 | const description = project.observations.find(o => o.startsWith('Description:'))?.split(':', 2)[1]?.trim();
519 | const startDate = project.observations.find(o => o.startsWith('StartDate:'))?.split(':', 2)[1]?.trim();
520 | const endDate = project.observations.find(o => o.startsWith('EndDate:'))?.split(':', 2)[1]?.trim();
521 | const priority = project.observations.find(o => o.startsWith('Priority:'))?.split(':', 2)[1]?.trim();
522 | const status = project.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'planning';
523 | const goal = project.observations.find(o => o.startsWith('Goal:'))?.split(':', 2)[1]?.trim();
524 | const budget = project.observations.find(o => o.startsWith('Budget:'))?.split(':', 2)[1]?.trim();
525 |
526 | // Find components of the project
527 | const components = graph.entities.filter(e => {
528 | return graph.relations.some(r =>
529 | r.from === e.name &&
530 | r.to === projectName &&
531 | r.relationType === 'part_of' &&
532 | e.entityType === 'component'
533 | );
534 | });
535 |
536 | // Find tasks for this project
537 | const tasks: Entity[] = [];
538 | for (const relation of graph.relations) {
539 | if (relation.relationType === 'part_of' && relation.to === projectName) {
540 | const task = graph.entities.find(e => e.name === relation.from && e.entityType === 'task');
541 | if (task) {
542 | tasks.push(task);
543 | }
544 | }
545 | }
546 |
547 | // Group tasks by status
548 | const tasksByStatus: { [status: string]: Entity[] } = {};
549 | for (const task of tasks) {
550 | const taskStatus = task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'not_started';
551 | if (!tasksByStatus[taskStatus]) {
552 | tasksByStatus[taskStatus] = [];
553 | }
554 | tasksByStatus[taskStatus].push(task);
555 | }
556 |
557 | // Find milestones for this project
558 | const milestones: Entity[] = [];
559 | for (const relation of graph.relations) {
560 | if (relation.relationType === 'part_of' && relation.to === projectName) {
561 | const milestone = graph.entities.find(e => e.name === relation.from && e.entityType === 'milestone');
562 | if (milestone) {
563 | milestones.push(milestone);
564 | }
565 | }
566 | }
567 |
568 | // Sort milestones by date
569 | milestones.sort((a, b) => {
570 | const aDate = a.observations.find(o => o.startsWith('Date:'))?.split(':', 2)[1]?.trim() || '';
571 | const bDate = b.observations.find(o => o.startsWith('Date:'))?.split(':', 2)[1]?.trim() || '';
572 | return new Date(aDate).getTime() - new Date(bDate).getTime();
573 | });
574 |
575 | // Find team members for this project
576 | const teamMembers: Entity[] = [];
577 | for (const relation of graph.relations) {
578 | if ((relation.relationType === 'assigned_to' || relation.relationType === 'manages' || relation.relationType === 'contributes_to') &&
579 | relation.to === projectName) {
580 | const teamMember = graph.entities.find(e => e.name === relation.from && e.entityType === 'teamMember');
581 | if (teamMember && !teamMembers.some(tm => tm.name === teamMember.name)) {
582 | teamMembers.push(teamMember);
583 | }
584 | }
585 | }
586 |
587 | // Find issues for this project
588 | const issues: Entity[] = [];
589 | for (const relation of graph.relations) {
590 | if (relation.relationType === 'part_of' && relation.to === projectName) {
591 | const issue = graph.entities.find(e => e.name === relation.from && e.entityType === 'issue');
592 | if (issue) {
593 | issues.push(issue);
594 | }
595 | }
596 | }
597 |
598 | // Group issues by status
599 | const issuesByStatus: { [status: string]: Entity[] } = {};
600 | for (const issue of issues) {
601 | const issueStatus = issue.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'identified';
602 | if (!issuesByStatus[issueStatus]) {
603 | issuesByStatus[issueStatus] = [];
604 | }
605 | issuesByStatus[issueStatus].push(issue);
606 | }
607 |
608 | // Find risks for this project
609 | const risks: Entity[] = [];
610 | for (const relation of graph.relations) {
611 | if (relation.relationType === 'part_of' && relation.to === projectName) {
612 | const risk = graph.entities.find(e => e.name === relation.from && e.entityType === 'risk');
613 | if (risk) {
614 | risks.push(risk);
615 | }
616 | }
617 | }
618 |
619 | // Find resources for this project
620 | const resources: Entity[] = [];
621 | for (const relation of graph.relations) {
622 | if (relation.relationType === 'part_of' && relation.to === projectName) {
623 | const resource = graph.entities.find(e => e.name === relation.from && e.entityType === 'resource');
624 | if (resource) {
625 | resources.push(resource);
626 | }
627 | }
628 | }
629 |
630 | // Find stakeholders for this project
631 | const stakeholders: Entity[] = [];
632 | for (const relation of graph.relations) {
633 | if (relation.relationType === 'stakeholder_of' && relation.to === projectName) {
634 | const stakeholder = graph.entities.find(e => e.name === relation.from && e.entityType === 'stakeholder');
635 | if (stakeholder) {
636 | stakeholders.push(stakeholder);
637 | }
638 | }
639 | }
640 |
641 | // Calculate task completion rate
642 | const completedTasks = tasks.filter(t =>
643 | t.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'completed'
644 | ).length;
645 | const taskCompletionRate = tasks.length > 0 ? (completedTasks / tasks.length) * 100 : 0;
646 |
647 | // Get upcoming milestones
648 | const today = new Date();
649 | const upcomingMilestones = milestones.filter(m => {
650 | const dateStr = m.observations.find(o => o.startsWith('Date:'))?.split(':', 2)[1]?.trim();
651 | if (dateStr) {
652 | const milestoneDate = new Date(dateStr);
653 | return milestoneDate >= today;
654 | }
655 | return false;
656 | });
657 |
658 | return {
659 | project,
660 | info: {
661 | description,
662 | startDate,
663 | endDate,
664 | priority,
665 | status,
666 | goal,
667 | budget
668 | },
669 | summary: {
670 | taskCount: tasks.length,
671 | completedTasks,
672 | taskCompletionRate: Math.round(taskCompletionRate),
673 | milestoneCount: milestones.length,
674 | teamMemberCount: teamMembers.length,
675 | issueCount: issues.length,
676 | riskCount: risks.length,
677 | componentCount: components.length
678 | },
679 | components,
680 | tasks,
681 | tasksByStatus,
682 | milestones,
683 | upcomingMilestones,
684 | teamMembers,
685 | issues,
686 | issuesByStatus,
687 | risks,
688 | resources,
689 | stakeholders
690 | };
691 | }
692 |
693 | // Visualizes dependencies between tasks, optionally to a specified depth
694 | async getTaskDependencies(taskName: string, depth: number = 2): Promise<any> {
695 | const graph = await this.loadGraph();
696 |
697 | // Find the task
698 | const task = graph.entities.find(e => e.name === taskName && e.entityType === 'task');
699 | if (!task) {
700 | throw new Error(`Task '${taskName}' not found`);
701 | }
702 |
703 | // Find the project this task belongs to
704 | let projectName: string | undefined;
705 | for (const relation of graph.relations) {
706 | if (relation.relationType === 'part_of' && relation.from === taskName) {
707 | const project = graph.entities.find(e => e.name === relation.to && e.entityType === 'project');
708 | if (project) {
709 | projectName = project.name;
710 | break;
711 | }
712 | }
713 | }
714 |
715 | // Initialize dependency tree
716 | interface DependencyNode {
717 | task: Entity;
718 | dependsOn: DependencyNode[];
719 | dependedOnBy: DependencyNode[];
720 | level: number;
721 | }
722 |
723 | const dependencyMap = new Map<string, DependencyNode>();
724 |
725 | // Helper function to add a task and its dependencies recursively
726 | const addDependencies = (taskEntity: Entity, currentLevel: number, direction: 'dependsOn' | 'dependedOnBy') => {
727 | if (currentLevel > depth) return;
728 |
729 | // Create node if it doesn't exist
730 | if (!dependencyMap.has(taskEntity.name)) {
731 | dependencyMap.set(taskEntity.name, {
732 | task: taskEntity,
733 | dependsOn: [],
734 | dependedOnBy: [],
735 | level: direction === 'dependsOn' ? currentLevel : 0
736 | });
737 | }
738 |
739 | const node = dependencyMap.get(taskEntity.name)!;
740 |
741 | // Update level if this path is shorter
742 | if (direction === 'dependsOn' && currentLevel < node.level) {
743 | node.level = currentLevel;
744 | }
745 |
746 | if (direction === 'dependsOn') {
747 | // Find tasks this task depends on
748 | for (const relation of graph.relations) {
749 | if (relation.relationType === 'depends_on' && relation.from === taskEntity.name) {
750 | const dependencyTask = graph.entities.find(e => e.name === relation.to && e.entityType === 'task');
751 | if (dependencyTask) {
752 | // Check if this dependency is already in the node's dependsOn list
753 | if (!node.dependsOn.some(d => d.task.name === dependencyTask.name)) {
754 | // Recursively add dependencies
755 | addDependencies(dependencyTask, currentLevel + 1, 'dependsOn');
756 |
757 | // Add this dependency to the node's dependsOn list
758 | const dependencyNode = dependencyMap.get(dependencyTask.name)!;
759 | node.dependsOn.push(dependencyNode);
760 |
761 | // Add the reverse relationship
762 | if (!dependencyNode.dependedOnBy.some(d => d.task.name === taskEntity.name)) {
763 | dependencyNode.dependedOnBy.push(node);
764 | }
765 | }
766 | }
767 | }
768 | }
769 | } else { // direction === 'dependedOnBy'
770 | // Find tasks that depend on this task
771 | for (const relation of graph.relations) {
772 | if (relation.relationType === 'depends_on' && relation.to === taskEntity.name) {
773 | const dependentTask = graph.entities.find(e => e.name === relation.from && e.entityType === 'task');
774 | if (dependentTask) {
775 | // Check if this dependent is already in the node's dependedOnBy list
776 | if (!node.dependedOnBy.some(d => d.task.name === dependentTask.name)) {
777 | // Recursively add dependents
778 | addDependencies(dependentTask, currentLevel + 1, 'dependedOnBy');
779 |
780 | // Add this dependent to the node's dependedOnBy list
781 | const dependentNode = dependencyMap.get(dependentTask.name)!;
782 | node.dependedOnBy.push(dependentNode);
783 |
784 | // Add the reverse relationship
785 | if (!dependentNode.dependsOn.some(d => d.task.name === taskEntity.name)) {
786 | dependentNode.dependsOn.push(node);
787 | }
788 | }
789 | }
790 | }
791 | }
792 | }
793 | };
794 |
795 | // Start with the main task and build the dependency tree in both directions
796 | addDependencies(task, 0, 'dependsOn');
797 | addDependencies(task, 0, 'dependedOnBy');
798 |
799 | // Convert to a serializable structure without circular references
800 | const serializableDependencies = Array.from(dependencyMap.values()).map(node => {
801 | const { task, level } = node;
802 |
803 | return {
804 | task,
805 | level,
806 | dependsOn: node.dependsOn.map(d => d.task.name),
807 | dependedOnBy: node.dependedOnBy.map(d => d.task.name),
808 | status: task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'not_started',
809 | dueDate: task.observations.find(o => o.startsWith('DueDate:'))?.split(':', 2)[1]?.trim(),
810 | assignee: this.getTaskAssignee(graph, task.name)
811 | };
812 | });
813 |
814 | // Sort by level (dependency depth)
815 | serializableDependencies.sort((a, b) => a.level - b.level);
816 |
817 | // Calculate the critical path
818 | const criticalPath = this.calculateCriticalPath(graph, serializableDependencies);
819 |
820 | return {
821 | task,
822 | projectName,
823 | dependencies: serializableDependencies,
824 | criticalPath,
825 | summary: {
826 | totalDependencies: serializableDependencies.length - 1, // Exclude the main task
827 | maxDepth: depth,
828 | blockedBy: serializableDependencies.filter(d =>
829 | d.task.name !== taskName &&
830 | d.status !== 'completed' &&
831 | task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() !== 'completed'
832 | ).length
833 | }
834 | };
835 | }
836 |
837 | // Helper to find the assignee of a task
838 | private getTaskAssignee(graph: KnowledgeGraph, taskName: string): string | undefined {
839 | for (const relation of graph.relations) {
840 | if (relation.relationType === 'assigned_to' && relation.from === taskName) {
841 | const teamMember = graph.entities.find(e => e.name === relation.to && e.entityType === 'teamMember');
842 | if (teamMember) {
843 | return teamMember.name;
844 | }
845 | }
846 | }
847 | return undefined;
848 | }
849 |
850 | // Helper to calculate the critical path
851 | private calculateCriticalPath(graph: KnowledgeGraph, dependencies: any[]): string[] {
852 | // Simple implementation - find the longest chain of dependencies
853 | // A more sophisticated implementation would account for task durations
854 |
855 | // Create an adjacency list
856 | const adjacencyList = new Map<string, string[]>();
857 |
858 | // Initialize the adjacency list for all tasks
859 | for (const dep of dependencies) {
860 | adjacencyList.set(dep.task.name, []);
861 | }
862 |
863 | // Populate the adjacency list with dependencies
864 | for (const dep of dependencies) {
865 | for (const dependsOn of dep.dependsOn) {
866 | const list = adjacencyList.get(dependsOn) || [];
867 | list.push(dep.task.name);
868 | adjacencyList.set(dependsOn, list);
869 | }
870 | }
871 |
872 | // Find tasks with no dependencies (starting points)
873 | const startNodes = dependencies
874 | .filter(dep => dep.dependsOn.length === 0)
875 | .map(dep => dep.task.name);
876 |
877 | // Find tasks that no other tasks depend on (end points)
878 | const endNodes = dependencies
879 | .filter(dep => dep.dependedOnBy.length === 0)
880 | .map(dep => dep.task.name);
881 |
882 | // If there are multiple start or end nodes, we need a more sophisticated algorithm
883 | // For simplicity, we'll just find the longest path from any start to any end
884 |
885 | // Find all paths from start to end
886 | const allPaths: string[][] = [];
887 |
888 | const findPaths = (current: string, path: string[] = []) => {
889 | const newPath = [...path, current];
890 |
891 | if (endNodes.includes(current)) {
892 | allPaths.push(newPath);
893 | return;
894 | }
895 |
896 | const nextNodes = adjacencyList.get(current) || [];
897 | for (const next of nextNodes) {
898 | // Avoid cycles
899 | if (!path.includes(next)) {
900 | findPaths(next, newPath);
901 | }
902 | }
903 | };
904 |
905 | for (const start of startNodes) {
906 | findPaths(start);
907 | }
908 |
909 | // Find the longest path
910 | allPaths.sort((a, b) => b.length - a.length);
911 |
912 | return allPaths.length > 0 ? allPaths[0] : [];
913 | }
914 |
915 | // See all tasks assigned to a team member
916 | async getTeamMemberAssignments(teamMemberName: string): Promise<any> {
917 | const graph = await this.loadGraph();
918 |
919 | // Find the team member
920 | const teamMember = graph.entities.find(e => e.name === teamMemberName && e.entityType === 'teamMember');
921 | if (!teamMember) {
922 | throw new Error(`Team member '${teamMemberName}' not found`);
923 | }
924 |
925 | // Extract team member info
926 | const role = teamMember.observations.find(o => o.startsWith('Role:'))?.split(':', 2)[1]?.trim();
927 | const skills = teamMember.observations.find(o => o.startsWith('Skills:'))?.split(':', 2)[1]?.trim();
928 | const availability = teamMember.observations.find(o => o.startsWith('Availability:'))?.split(':', 2)[1]?.trim();
929 |
930 | // Find tasks assigned to this team member
931 | interface TaskAssignment {
932 | task: Entity;
933 | project: Entity | undefined;
934 | dueDate: string | undefined;
935 | status: string;
936 | priority: string | undefined;
937 | }
938 |
939 | const assignedTasks: TaskAssignment[] = [];
940 |
941 | // Find assigned tasks through 'assigned_to' relations
942 | for (const relation of graph.relations) {
943 | if (relation.relationType === 'assigned_to' && relation.to === teamMemberName) {
944 | const task = graph.entities.find(e => e.name === relation.from && e.entityType === 'task');
945 | if (task) {
946 | // Find the project this task belongs to
947 | let project: Entity | undefined;
948 | for (const taskRelation of graph.relations) {
949 | if (taskRelation.relationType === 'part_of' && taskRelation.from === task.name) {
950 | project = graph.entities.find(e => e.name === taskRelation.to && e.entityType === 'project');
951 | if (project) break;
952 | }
953 | }
954 |
955 | // Extract task info
956 | const dueDate = task.observations.find(o => o.startsWith('DueDate:'))?.split(':', 2)[1]?.trim();
957 | const status = task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'not_started';
958 | const priority = task.observations.find(o => o.startsWith('Priority:'))?.split(':', 2)[1]?.trim();
959 |
960 | assignedTasks.push({
961 | task,
962 | project,
963 | dueDate,
964 | status,
965 | priority
966 | });
967 | }
968 | }
969 | }
970 |
971 | // Sort tasks by due date
972 | assignedTasks.sort((a, b) => {
973 | if (!a.dueDate) return 1;
974 | if (!b.dueDate) return -1;
975 | return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
976 | });
977 |
978 | // Find projects this team member is involved in
979 | const projects: Entity[] = [];
980 | for (const relation of graph.relations) {
981 | if ((relation.relationType === 'manages' || relation.relationType === 'contributes_to') &&
982 | relation.from === teamMemberName) {
983 | const project = graph.entities.find(e => e.name === relation.to && e.entityType === 'project');
984 | if (project && !projects.some(p => p.name === project.name)) {
985 | projects.push(project);
986 | }
987 | }
988 | }
989 |
990 | // Group tasks by project
991 | const tasksByProject: { [projectName: string]: TaskAssignment[] } = {};
992 | for (const assignment of assignedTasks) {
993 | const projectName = assignment.project?.name || 'Unassigned';
994 | if (!tasksByProject[projectName]) {
995 | tasksByProject[projectName] = [];
996 | }
997 | tasksByProject[projectName].push(assignment);
998 | }
999 |
1000 | // Group tasks by status
1001 | const tasksByStatus: { [status: string]: TaskAssignment[] } = {};
1002 | for (const assignment of assignedTasks) {
1003 | if (!tasksByStatus[assignment.status]) {
1004 | tasksByStatus[assignment.status] = [];
1005 | }
1006 | tasksByStatus[assignment.status].push(assignment);
1007 | }
1008 |
1009 | // Calculate workload metrics
1010 | const completedTasks = assignedTasks.filter(t => t.status === 'completed').length;
1011 | const inProgressTasks = assignedTasks.filter(t => t.status === 'in_progress').length;
1012 | const notStartedTasks = assignedTasks.filter(t => t.status === 'not_started').length;
1013 | const blockedTasks = assignedTasks.filter(t => t.status === 'blocked').length;
1014 |
1015 | // Calculate upcoming deadlines
1016 | const today = new Date();
1017 | const upcomingDeadlines = assignedTasks
1018 | .filter(t => {
1019 | if (!t.dueDate || t.status === 'completed') return false;
1020 | const dueDate = new Date(t.dueDate);
1021 | const daysUntilDue = Math.ceil((dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
1022 | return daysUntilDue >= 0 && daysUntilDue <= 7; // Within the next week
1023 | })
1024 | .sort((a, b) => {
1025 | return new Date(a.dueDate!).getTime() - new Date(b.dueDate!).getTime();
1026 | });
1027 |
1028 | // Find overdue tasks
1029 | const overdueTasks = assignedTasks
1030 | .filter(t => {
1031 | if (!t.dueDate || t.status === 'completed') return false;
1032 | const dueDate = new Date(t.dueDate);
1033 | return dueDate < today;
1034 | })
1035 | .sort((a, b) => {
1036 | return new Date(a.dueDate!).getTime() - new Date(b.dueDate!).getTime();
1037 | });
1038 |
1039 | return {
1040 | teamMember,
1041 | info: {
1042 | role,
1043 | skills,
1044 | availability
1045 | },
1046 | workload: {
1047 | totalTasks: assignedTasks.length,
1048 | completedTasks,
1049 | inProgressTasks,
1050 | notStartedTasks,
1051 | blockedTasks,
1052 | completionRate: assignedTasks.length > 0 ?
1053 | Math.round((completedTasks / assignedTasks.length) * 100) : 0
1054 | },
1055 | assignedTasks,
1056 | tasksByProject,
1057 | tasksByStatus,
1058 | projects,
1059 | upcomingDeadlines,
1060 | overdueTasks
1061 | };
1062 | }
1063 |
1064 | // Track progress toward project milestones
1065 | async getMilestoneProgress(projectName: string, milestoneName?: string): Promise<any> {
1066 | const graph = await this.loadGraph();
1067 |
1068 | // Find the project
1069 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
1070 | if (!project) {
1071 | throw new Error(`Project '${projectName}' not found`);
1072 | }
1073 |
1074 | // Find milestones for this project, or a specific milestone if provided
1075 | const milestones = milestoneName
1076 | ? graph.entities.filter(e =>
1077 | e.name === milestoneName &&
1078 | e.entityType === 'milestone' &&
1079 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1080 | )
1081 | : graph.entities.filter(e =>
1082 | e.entityType === 'milestone' &&
1083 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1084 | );
1085 |
1086 | if (milestoneName && milestones.length === 0) {
1087 | throw new Error(`Milestone '${milestoneName}' not found in project '${projectName}'`);
1088 | }
1089 |
1090 | // Process each milestone
1091 | const milestoneProgress: any[] = [];
1092 |
1093 | for (const milestone of milestones) {
1094 | // Extract milestone info
1095 | const description = milestone.observations.find(o => o.startsWith('Description:'))?.split(':', 2)[1]?.trim();
1096 | const date = milestone.observations.find(o => o.startsWith('Date:'))?.split(':', 2)[1]?.trim();
1097 | const status = milestone.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'planned';
1098 | const criteria = milestone.observations.find(o => o.startsWith('Criteria:'))?.split(':', 2)[1]?.trim();
1099 |
1100 | // Find related tasks
1101 | const relatedTasks: Entity[] = [];
1102 | for (const relation of graph.relations) {
1103 | if (relation.relationType === 'required_for' && relation.to === milestone.name) {
1104 | const task = graph.entities.find(e => e.name === relation.from && e.entityType === 'task');
1105 | if (task) {
1106 | relatedTasks.push(task);
1107 | }
1108 | }
1109 | }
1110 |
1111 | // Calculate task completion for this milestone
1112 | const completedTasks = relatedTasks.filter(task =>
1113 | task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'completed'
1114 | ).length;
1115 |
1116 | const completionPercentage = relatedTasks.length > 0
1117 | ? Math.round((completedTasks / relatedTasks.length) * 100)
1118 | : status === 'reached' ? 100 : 0;
1119 |
1120 | // Calculate days until/since milestone
1121 | let daysRemaining: number | null = null;
1122 | let isOverdue = false;
1123 |
1124 | if (date) {
1125 | const milestoneDate = new Date(date);
1126 | const today = new Date();
1127 | const diffTime = milestoneDate.getTime() - today.getTime();
1128 | daysRemaining = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
1129 | isOverdue = diffTime < 0 && status !== 'reached' && status !== 'missed';
1130 | }
1131 |
1132 | // Find blockers (incomplete tasks that are required)
1133 | const blockers = relatedTasks.filter(task => {
1134 | const taskStatus = task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim();
1135 | return taskStatus !== 'completed' && taskStatus !== 'cancelled';
1136 | });
1137 |
1138 | milestoneProgress.push({
1139 | milestone,
1140 | info: {
1141 | description,
1142 | date,
1143 | status,
1144 | criteria
1145 | },
1146 | progress: {
1147 | totalTasks: relatedTasks.length,
1148 | completedTasks,
1149 | completionPercentage,
1150 | daysRemaining,
1151 | isOverdue
1152 | },
1153 | relatedTasks,
1154 | blockers
1155 | });
1156 | }
1157 |
1158 | // Sort milestones by date
1159 | milestoneProgress.sort((a, b) => {
1160 | if (!a.info.date) return 1;
1161 | if (!b.info.date) return -1;
1162 | return new Date(a.info.date).getTime() - new Date(b.info.date).getTime();
1163 | });
1164 |
1165 | // Calculate overall project milestone progress
1166 | const totalMilestones = milestoneProgress.length;
1167 | const reachedMilestones = milestoneProgress.filter(m => m.info.status === 'reached').length;
1168 | const averageCompletion = totalMilestones > 0
1169 | ? milestoneProgress.reduce((sum, m) => sum + m.progress.completionPercentage, 0) / totalMilestones
1170 | : 0;
1171 |
1172 | return {
1173 | project,
1174 | milestones: milestoneProgress,
1175 | summary: {
1176 | totalMilestones,
1177 | reachedMilestones,
1178 | milestoneCompletionRate: totalMilestones > 0 ? Math.round((reachedMilestones / totalMilestones) * 100) : 0,
1179 | averageCompletion: Math.round(averageCompletion),
1180 | nextMilestone: milestoneProgress.find(m =>
1181 | m.info.status !== 'reached' && m.info.status !== 'missed'
1182 | ),
1183 | overdueMilestones: milestoneProgress.filter(m => m.progress.isOverdue).length
1184 | }
1185 | };
1186 | }
1187 |
1188 | // Create a timeline view with important dates
1189 | async getProjectTimeline(projectName: string): Promise<any> {
1190 | const graph = await this.loadGraph();
1191 |
1192 | // Find the project
1193 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
1194 | if (!project) {
1195 | throw new Error(`Project '${projectName}' not found`);
1196 | }
1197 |
1198 | // Extract project dates
1199 | const projectStartDate = project.observations.find(o => o.startsWith('StartDate:'))?.split(':', 2)[1]?.trim();
1200 | const projectEndDate = project.observations.find(o => o.startsWith('EndDate:'))?.split(':', 2)[1]?.trim();
1201 |
1202 | // Create a timeline of all dated events
1203 | interface TimelineEvent {
1204 | date: Date;
1205 | entity: Entity;
1206 | eventType: 'milestone' | 'task' | 'meeting' | 'project_start' | 'project_end';
1207 | description?: string;
1208 | status?: string;
1209 | }
1210 |
1211 | const timelineEvents: TimelineEvent[] = [];
1212 |
1213 | // Add project start and end dates
1214 | if (projectStartDate) {
1215 | timelineEvents.push({
1216 | date: new Date(projectStartDate),
1217 | entity: project,
1218 | eventType: 'project_start',
1219 | description: 'Project Start'
1220 | });
1221 | }
1222 |
1223 | if (projectEndDate) {
1224 | timelineEvents.push({
1225 | date: new Date(projectEndDate),
1226 | entity: project,
1227 | eventType: 'project_end',
1228 | description: 'Project End'
1229 | });
1230 | }
1231 |
1232 | // Find milestones for this project
1233 | const milestones = graph.entities.filter(e =>
1234 | e.entityType === 'milestone' &&
1235 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1236 | );
1237 |
1238 | // Add milestones to timeline
1239 | for (const milestone of milestones) {
1240 | const date = milestone.observations.find(o => o.startsWith('Date:'))?.split(':', 2)[1]?.trim();
1241 | if (date) {
1242 | timelineEvents.push({
1243 | date: new Date(date),
1244 | entity: milestone,
1245 | eventType: 'milestone',
1246 | description: milestone.observations.find(o => o.startsWith('Description:'))?.split(':', 2)[1]?.trim(),
1247 | status: milestone.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim()
1248 | });
1249 | }
1250 | }
1251 |
1252 | // Find tasks with due dates
1253 | const tasks = graph.entities.filter(e =>
1254 | e.entityType === 'task' &&
1255 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1256 | );
1257 |
1258 | // Add tasks to timeline
1259 | for (const task of tasks) {
1260 | const dueDate = task.observations.find(o => o.startsWith('DueDate:'))?.split(':', 2)[1]?.trim();
1261 | if (dueDate) {
1262 | timelineEvents.push({
1263 | date: new Date(dueDate),
1264 | entity: task,
1265 | eventType: 'task',
1266 | description: task.observations.find(o => o.startsWith('Description:'))?.split(':', 2)[1]?.trim(),
1267 | status: task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim()
1268 | });
1269 | }
1270 | }
1271 |
1272 | // Sort timeline events by date
1273 | timelineEvents.sort((a, b) => a.date.getTime() - b.date.getTime());
1274 |
1275 | // Calculate time spans between events
1276 | const timelineWithSpans = timelineEvents.map((event, index) => {
1277 | let daysFromStart = 0;
1278 | let daysToNext = 0;
1279 |
1280 | if (index === 0 && projectStartDate) {
1281 | // First event relative to project start
1282 | daysFromStart = 0;
1283 | } else if (index > 0) {
1284 | // Days from previous event
1285 | const prevDate = timelineEvents[index - 1].date;
1286 | daysFromStart = Math.round((event.date.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24));
1287 | }
1288 |
1289 | if (index < timelineEvents.length - 1) {
1290 | // Days until next event
1291 | const nextDate = timelineEvents[index + 1].date;
1292 | daysToNext = Math.round((nextDate.getTime() - event.date.getTime()) / (1000 * 60 * 60 * 24));
1293 | }
1294 |
1295 | return {
1296 | ...event,
1297 | dateString: event.date.toISOString().split('T')[0],
1298 | daysFromStart,
1299 | daysToNext
1300 | };
1301 | });
1302 |
1303 | // Find the current position in the timeline
1304 | const today = new Date();
1305 | let currentPosition = -1;
1306 |
1307 | for (let i = 0; i < timelineEvents.length; i++) {
1308 | if (timelineEvents[i].date >= today) {
1309 | currentPosition = i;
1310 | break;
1311 | }
1312 | }
1313 |
1314 | // If we're past all events, set current position to the last event
1315 | if (currentPosition === -1 && timelineEvents.length > 0) {
1316 | currentPosition = timelineEvents.length - 1;
1317 | }
1318 |
1319 | // Calculate overall project progress based on timeline
1320 | let progressPercentage = 0;
1321 |
1322 | if (timelineEvents.length >= 2) {
1323 | const startDate = timelineEvents[0].date;
1324 | const endDate = timelineEvents[timelineEvents.length - 1].date;
1325 | const totalDuration = endDate.getTime() - startDate.getTime();
1326 |
1327 | if (totalDuration > 0) {
1328 | const elapsed = today.getTime() - startDate.getTime();
1329 | progressPercentage = Math.min(100, Math.max(0, Math.round((elapsed / totalDuration) * 100)));
1330 | }
1331 | }
1332 |
1333 | return {
1334 | project,
1335 | timeline: timelineWithSpans,
1336 | currentPosition,
1337 | progressPercentage,
1338 | projectDuration: timelineEvents.length >= 2 ?
1339 | Math.round((timelineEvents[timelineEvents.length - 1].date.getTime() - timelineEvents[0].date.getTime()) / (1000 * 60 * 60 * 24)) : 0,
1340 | upcomingEvents: timelineWithSpans.filter(e => e.date >= today).slice(0, 5)
1341 | };
1342 | }
1343 |
1344 | // Shows how resources are allocated across the project
1345 | async getResourceAllocation(projectName: string, resourceName?: string): Promise<any> {
1346 | const graph = await this.loadGraph();
1347 |
1348 | // Find the project
1349 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
1350 | if (!project) {
1351 | throw new Error(`Project '${projectName}' not found`);
1352 | }
1353 |
1354 | // Find resources for this project, or a specific resource if provided
1355 | const resources = resourceName
1356 | ? graph.entities.filter(e =>
1357 | e.name === resourceName &&
1358 | e.entityType === 'resource' &&
1359 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1360 | )
1361 | : graph.entities.filter(e =>
1362 | e.entityType === 'resource' &&
1363 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1364 | );
1365 |
1366 | if (resourceName && resources.length === 0) {
1367 | throw new Error(`Resource '${resourceName}' not found in project '${projectName}'`);
1368 | }
1369 |
1370 | // Process each resource
1371 | const resourceAllocations = [];
1372 |
1373 | for (const resource of resources) {
1374 | // Extract resource info
1375 | const type = resource.observations.find(o => o.startsWith('Type:'))?.split(':', 2)[1]?.trim();
1376 | const availability = resource.observations.find(o => o.startsWith('Availability:'))?.split(':', 2)[1]?.trim();
1377 | const capacity = resource.observations.find(o => o.startsWith('Capacity:'))?.split(':', 2)[1]?.trim();
1378 | const cost = resource.observations.find(o => o.startsWith('Cost:'))?.split(':', 2)[1]?.trim();
1379 |
1380 | // Find tasks that use this resource
1381 | const assignedTasks: Entity[] = [];
1382 | for (const relation of graph.relations) {
1383 | if (relation.relationType === 'requires' && relation.to === resource.name) {
1384 | const task = graph.entities.find(e => e.name === relation.from && e.entityType === 'task');
1385 | if (task) {
1386 | assignedTasks.push(task);
1387 | }
1388 | }
1389 | }
1390 |
1391 | // Sort tasks by due date
1392 | assignedTasks.sort((a, b) => {
1393 | const aDate = a.observations.find(o => o.startsWith('DueDate:'))?.split(':', 2)[1]?.trim() || '';
1394 | const bDate = b.observations.find(o => o.startsWith('DueDate:'))?.split(':', 2)[1]?.trim() || '';
1395 | if (!aDate) return 1;
1396 | if (!bDate) return -1;
1397 | return new Date(aDate).getTime() - new Date(bDate).getTime();
1398 | });
1399 |
1400 | // Group tasks by status
1401 | const tasksByStatus: { [status: string]: Entity[] } = {};
1402 | for (const task of assignedTasks) {
1403 | const status = task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'not_started';
1404 | if (!tasksByStatus[status]) {
1405 | tasksByStatus[status] = [];
1406 | }
1407 | tasksByStatus[status].push(task);
1408 | }
1409 |
1410 | // Find team members using this resource
1411 | const teamMembers: Entity[] = [];
1412 | for (const relation of graph.relations) {
1413 | if (relation.relationType === 'uses' && relation.to === resource.name) {
1414 | const teamMember = graph.entities.find(e => e.name === relation.from && e.entityType === 'teamMember');
1415 | if (teamMember) {
1416 | teamMembers.push(teamMember);
1417 | }
1418 | }
1419 | }
1420 |
1421 | // Calculate usage percentage based on assigned tasks
1422 | const totalTasks = assignedTasks.length;
1423 | const inProgressTasks = tasksByStatus['in_progress']?.length || 0;
1424 |
1425 | // Simple formula for usage percentage
1426 | const usagePercentage = capacity
1427 | ? Math.min(100, Math.round((inProgressTasks / parseInt(capacity)) * 100))
1428 | : totalTasks > 0 ? 50 : 0; // Default to 50% if we have tasks but no capacity
1429 |
1430 | resourceAllocations.push({
1431 | resource,
1432 | info: {
1433 | type,
1434 | availability,
1435 | capacity,
1436 | cost
1437 | },
1438 | usage: {
1439 | totalTasks,
1440 | inProgressTasks,
1441 | usagePercentage
1442 | },
1443 | assignedTasks,
1444 | tasksByStatus,
1445 | teamMembers
1446 | });
1447 | }
1448 |
1449 | // Sort resources by usage percentage (descending)
1450 | resourceAllocations.sort((a, b) => b.usage.usagePercentage - a.usage.usagePercentage);
1451 |
1452 | // Identify overallocated resources
1453 | const overallocatedResources = resourceAllocations.filter(r => r.usage.usagePercentage > 90);
1454 |
1455 | // Identify underutilized resources
1456 | const underutilizedResources = resourceAllocations.filter(r => r.usage.usagePercentage < 20 && r.usage.totalTasks > 0);
1457 |
1458 | return {
1459 | project,
1460 | resources: resourceAllocations,
1461 | summary: {
1462 | totalResources: resources.length,
1463 | overallocatedCount: overallocatedResources.length,
1464 | underutilizedCount: underutilizedResources.length
1465 | },
1466 | overallocatedResources,
1467 | underutilizedResources
1468 | };
1469 | }
1470 |
1471 | // Identifies potential risks and their impact
1472 | async getProjectRisks(projectName: string): Promise<any> {
1473 | const graph = await this.loadGraph();
1474 |
1475 | // Find the project
1476 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
1477 | if (!project) {
1478 | throw new Error(`Project '${projectName}' not found`);
1479 | }
1480 |
1481 | // Find risks for this project
1482 | const risks = graph.entities.filter(e =>
1483 | e.entityType === 'risk' &&
1484 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1485 | );
1486 |
1487 | // Process each risk
1488 | const processedRisks = [];
1489 |
1490 | for (const risk of risks) {
1491 | // Extract risk info
1492 | const description = risk.observations.find(o => o.startsWith('Description:'))?.split(':', 2)[1]?.trim();
1493 | const likelihood = risk.observations.find(o => o.startsWith('Likelihood:'))?.split(':', 2)[1]?.trim();
1494 | const impact = risk.observations.find(o => o.startsWith('Impact:'))?.split(':', 2)[1]?.trim();
1495 | const status = risk.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'identified';
1496 | const mitigation = risk.observations.find(o => o.startsWith('Mitigation:'))?.split(':', 2)[1]?.trim();
1497 |
1498 | // Calculate risk score (if likelihood and impact are numerical)
1499 | let riskScore: number | undefined;
1500 |
1501 | if (likelihood && impact) {
1502 | const likelihoodValue = parseInt(likelihood);
1503 | const impactValue = parseInt(impact);
1504 | if (!isNaN(likelihoodValue) && !isNaN(impactValue)) {
1505 | riskScore = likelihoodValue * impactValue;
1506 | }
1507 | }
1508 |
1509 | // Find components or tasks affected by this risk
1510 | const affectedEntities: Entity[] = [];
1511 | for (const relation of graph.relations) {
1512 | if (relation.relationType === 'impacted_by' && relation.to === risk.name) {
1513 | const entity = graph.entities.find(e => e.name === relation.from);
1514 | if (entity) {
1515 | affectedEntities.push(entity);
1516 | }
1517 | }
1518 | }
1519 |
1520 | processedRisks.push({
1521 | risk,
1522 | info: {
1523 | description,
1524 | likelihood,
1525 | impact,
1526 | status,
1527 | mitigation,
1528 | riskScore
1529 | },
1530 | affectedEntities
1531 | });
1532 | }
1533 |
1534 | // Sort risks by risk score (descending)
1535 | processedRisks.sort((a, b) => {
1536 | if (a.info.riskScore === undefined) return 1;
1537 | if (b.info.riskScore === undefined) return -1;
1538 | return b.info.riskScore - a.info.riskScore;
1539 | });
1540 |
1541 | // Group risks by status
1542 | const risksByStatus: { [status: string]: any[] } = {};
1543 | for (const processedRisk of processedRisks) {
1544 | const status = processedRisk.info.status;
1545 | if (!risksByStatus[status]) {
1546 | risksByStatus[status] = [];
1547 | }
1548 | risksByStatus[status].push(processedRisk);
1549 | }
1550 |
1551 | // Identify high-priority risks
1552 | const highPriorityRisks = processedRisks.filter(r => {
1553 | if (r.info.riskScore !== undefined) {
1554 | return r.info.riskScore >= 15; // Threshold for high priority
1555 | }
1556 | return r.info.impact === 'high' || r.info.likelihood === 'high';
1557 | });
1558 |
1559 | return {
1560 | project,
1561 | risks: processedRisks,
1562 | risksByStatus,
1563 | summary: {
1564 | totalRisks: risks.length,
1565 | highPriorityCount: highPriorityRisks.length,
1566 | mitigatedCount: risksByStatus['mitigating']?.length || 0,
1567 | avoidedCount: risksByStatus['avoided']?.length || 0,
1568 | acceptedCount: risksByStatus['accepted']?.length || 0,
1569 | occurredCount: risksByStatus['occurred']?.length || 0
1570 | },
1571 | highPriorityRisks
1572 | };
1573 | }
1574 |
1575 | // Find connections between different projects
1576 | async findRelatedProjects(projectName: string, depth: number = 1): Promise<any> {
1577 | const graph = await this.loadGraph();
1578 |
1579 | // Find the project
1580 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
1581 | if (!project) {
1582 | throw new Error(`Project '${projectName}' not found`);
1583 | }
1584 |
1585 | interface ProjectConnection {
1586 | project: Entity;
1587 | connectionType: string;
1588 | connectionStrength: number;
1589 | sharedEntities: {
1590 | teamMembers: Entity[];
1591 | dependencies: Entity[];
1592 | resources: Entity[];
1593 | stakeholders: Entity[];
1594 | };
1595 | }
1596 |
1597 | const relatedProjects: ProjectConnection[] = [];
1598 | const processedProjects = new Set<string>([projectName]);
1599 |
1600 | // Helper function to find connections between projects
1601 | const findConnections = (currentProjectName: string, currentDepth: number) => {
1602 | if (currentDepth > depth) return;
1603 |
1604 | // Find all other projects
1605 | const otherProjects = graph.entities.filter(e =>
1606 | e.entityType === 'project' &&
1607 | e.name !== currentProjectName &&
1608 | !processedProjects.has(e.name)
1609 | );
1610 |
1611 | for (const otherProject of otherProjects) {
1612 | // Add to processed set to avoid cycles
1613 | processedProjects.add(otherProject.name);
1614 |
1615 | // Find shared team members
1616 | const sharedTeamMembers: Entity[] = [];
1617 | const projectTeamMembers = new Set<string>();
1618 | const otherProjectTeamMembers = new Set<string>();
1619 |
1620 | // Get team members for current project
1621 | for (const relation of graph.relations) {
1622 | if ((relation.relationType === 'assigned_to' || relation.relationType === 'contributes_to' || relation.relationType === 'manages') &&
1623 | relation.to === currentProjectName) {
1624 | const teamMember = graph.entities.find(e => e.name === relation.from && e.entityType === 'teamMember');
1625 | if (teamMember) {
1626 | projectTeamMembers.add(teamMember.name);
1627 | }
1628 | }
1629 | }
1630 |
1631 | // Get team members for other project
1632 | for (const relation of graph.relations) {
1633 | if ((relation.relationType === 'assigned_to' || relation.relationType === 'contributes_to' || relation.relationType === 'manages') &&
1634 | relation.to === otherProject.name) {
1635 | const teamMember = graph.entities.find(e => e.name === relation.from && e.entityType === 'teamMember');
1636 | if (teamMember) {
1637 | otherProjectTeamMembers.add(teamMember.name);
1638 | if (projectTeamMembers.has(teamMember.name)) {
1639 | sharedTeamMembers.push(teamMember);
1640 | }
1641 | }
1642 | }
1643 | }
1644 |
1645 | // Find shared resources
1646 | const sharedResources: Entity[] = [];
1647 | const projectResources = new Set<string>();
1648 | const otherProjectResources = new Set<string>();
1649 |
1650 | // Get resources for current project
1651 | for (const relation of graph.relations) {
1652 | if (relation.relationType === 'part_of' && relation.to === currentProjectName) {
1653 | const resource = graph.entities.find(e => e.name === relation.from && e.entityType === 'resource');
1654 | if (resource) {
1655 | projectResources.add(resource.name);
1656 | }
1657 | }
1658 | }
1659 |
1660 | // Get resources for other project
1661 | for (const relation of graph.relations) {
1662 | if (relation.relationType === 'part_of' && relation.to === otherProject.name) {
1663 | const resource = graph.entities.find(e => e.name === relation.from && e.entityType === 'resource');
1664 | if (resource) {
1665 | otherProjectResources.add(resource.name);
1666 | if (projectResources.has(resource.name)) {
1667 | sharedResources.push(resource);
1668 | }
1669 | }
1670 | }
1671 | }
1672 |
1673 | // Find shared stakeholders
1674 | const sharedStakeholders: Entity[] = [];
1675 | const projectStakeholders = new Set<string>();
1676 | const otherProjectStakeholders = new Set<string>();
1677 |
1678 | // Get stakeholders for current project
1679 | for (const relation of graph.relations) {
1680 | if (relation.relationType === 'stakeholder_of' && relation.to === currentProjectName) {
1681 | const stakeholder = graph.entities.find(e => e.name === relation.from && e.entityType === 'stakeholder');
1682 | if (stakeholder) {
1683 | projectStakeholders.add(stakeholder.name);
1684 | }
1685 | }
1686 | }
1687 |
1688 | // Get stakeholders for other project
1689 | for (const relation of graph.relations) {
1690 | if (relation.relationType === 'stakeholder_of' && relation.to === otherProject.name) {
1691 | const stakeholder = graph.entities.find(e => e.name === relation.from && e.entityType === 'stakeholder');
1692 | if (stakeholder) {
1693 | otherProjectStakeholders.add(stakeholder.name);
1694 | if (projectStakeholders.has(stakeholder.name)) {
1695 | sharedStakeholders.push(stakeholder);
1696 | }
1697 | }
1698 | }
1699 | }
1700 |
1701 | // Find dependencies between projects
1702 | const dependencies: Entity[] = [];
1703 |
1704 | // Check for 'depends_on' relations between projects
1705 | for (const relation of graph.relations) {
1706 | if (relation.relationType === 'depends_on') {
1707 | if (relation.from === currentProjectName && relation.to === otherProject.name) {
1708 | dependencies.push(otherProject);
1709 | } else if (relation.from === otherProject.name && relation.to === currentProjectName) {
1710 | dependencies.push(project);
1711 | }
1712 | }
1713 | }
1714 |
1715 | // Calculate connection strength (simple formula based on shared entities)
1716 | const connectionStrength =
1717 | (sharedTeamMembers.length * 2) + // Team members have higher weight
1718 | (sharedResources.length * 1.5) + // Resources are also important
1719 | (dependencies.length * 3) + // Dependencies have highest weight
1720 | (sharedStakeholders.length * 1); // Stakeholders have standard weight
1721 |
1722 | // Determine primary connection type
1723 | let connectionType = 'related';
1724 |
1725 | if (dependencies.length > 0) {
1726 | connectionType = 'dependency';
1727 | } else if (sharedTeamMembers.length > 0) {
1728 | connectionType = 'shared_team';
1729 | } else if (sharedResources.length > 0) {
1730 | connectionType = 'shared_resources';
1731 | } else if (sharedStakeholders.length > 0) {
1732 | connectionType = 'shared_stakeholders';
1733 | }
1734 |
1735 | // Only add connections with some relationship
1736 | if (connectionStrength > 0) {
1737 | relatedProjects.push({
1738 | project: otherProject,
1739 | connectionType,
1740 | connectionStrength,
1741 | sharedEntities: {
1742 | teamMembers: sharedTeamMembers,
1743 | dependencies,
1744 | resources: sharedResources,
1745 | stakeholders: sharedStakeholders
1746 | }
1747 | });
1748 |
1749 | // Recursively find connections for this project (up to the specified depth)
1750 | findConnections(otherProject.name, currentDepth + 1);
1751 | }
1752 | }
1753 | };
1754 |
1755 | // Start the recursive search
1756 | findConnections(projectName, 1);
1757 |
1758 | // Sort related projects by connection strength
1759 | relatedProjects.sort((a, b) => b.connectionStrength - a.connectionStrength);
1760 |
1761 | return {
1762 | project,
1763 | relatedProjects,
1764 | summary: {
1765 | totalRelated: relatedProjects.length,
1766 | byConnectionType: {
1767 | dependency: relatedProjects.filter(p => p.connectionType === 'dependency').length,
1768 | shared_team: relatedProjects.filter(p => p.connectionType === 'shared_team').length,
1769 | shared_resources: relatedProjects.filter(p => p.connectionType === 'shared_resources').length,
1770 | shared_stakeholders: relatedProjects.filter(p => p.connectionType === 'shared_stakeholders').length
1771 | },
1772 | maxDepth: depth
1773 | }
1774 | };
1775 | }
1776 |
1777 | // Get decision log for a project
1778 | async getDecisionLog(projectName: string): Promise<any> {
1779 | const graph = await this.loadGraph();
1780 |
1781 | // Find the project
1782 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
1783 | if (!project) {
1784 | throw new Error(`Project '${projectName}' not found`);
1785 | }
1786 |
1787 | // Find decisions for this project
1788 | const decisions = graph.entities.filter(e =>
1789 | e.entityType === 'decision' &&
1790 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1791 | );
1792 |
1793 | // Process each decision
1794 | const processedDecisions = [];
1795 |
1796 | for (const decision of decisions) {
1797 | // Extract decision info
1798 | const description = decision.observations.find(o => o.startsWith('Description:'))?.split(':', 2)[1]?.trim();
1799 | const date = decision.observations.find(o => o.startsWith('Date:'))?.split(':', 2)[1]?.trim();
1800 | const status = decision.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'proposed';
1801 | const rationale = decision.observations.find(o => o.startsWith('Rationale:'))?.split(':', 2)[1]?.trim();
1802 | const alternatives = decision.observations.find(o => o.startsWith('Alternatives:'))?.split(':', 2)[1]?.trim();
1803 |
1804 | // Find team members involved in this decision
1805 | const involvedTeamMembers: Entity[] = [];
1806 | for (const relation of graph.relations) {
1807 | if (relation.relationType === 'created_by' && relation.from === decision.name) {
1808 | const teamMember = graph.entities.find(e => e.name === relation.to && e.entityType === 'teamMember');
1809 | if (teamMember) {
1810 | involvedTeamMembers.push(teamMember);
1811 | }
1812 | }
1813 | }
1814 |
1815 | // Find entities affected by this decision
1816 | const affectedEntities: Entity[] = [];
1817 | for (const relation of graph.relations) {
1818 | if (relation.relationType === 'impacted_by' && relation.to === decision.name) {
1819 | const entity = graph.entities.find(e => e.name === relation.from);
1820 | if (entity) {
1821 | affectedEntities.push(entity);
1822 | }
1823 | }
1824 | }
1825 |
1826 | processedDecisions.push({
1827 | decision,
1828 | info: {
1829 | description,
1830 | date,
1831 | status,
1832 | rationale,
1833 | alternatives
1834 | },
1835 | involvedTeamMembers,
1836 | affectedEntities
1837 | });
1838 | }
1839 |
1840 | // Sort decisions by date (most recent first)
1841 | processedDecisions.sort((a, b) => {
1842 | if (!a.info.date) return 1;
1843 | if (!b.info.date) return -1;
1844 | return new Date(b.info.date).getTime() - new Date(a.info.date).getTime();
1845 | });
1846 |
1847 | // Group decisions by status
1848 | const decisionsByStatus: { [status: string]: any[] } = {};
1849 | for (const processedDecision of processedDecisions) {
1850 | const status = processedDecision.info.status;
1851 | if (!decisionsByStatus[status]) {
1852 | decisionsByStatus[status] = [];
1853 | }
1854 | decisionsByStatus[status].push(processedDecision);
1855 | }
1856 |
1857 | return {
1858 | project,
1859 | decisions: processedDecisions,
1860 | decisionsByStatus,
1861 | summary: {
1862 | totalDecisions: decisions.length,
1863 | approvedCount: decisionsByStatus['approved']?.length || 0,
1864 | implementedCount: decisionsByStatus['implemented']?.length || 0,
1865 | rejectedCount: decisionsByStatus['rejected']?.length || 0,
1866 | proposedCount: decisionsByStatus['proposed']?.length || 0
1867 | }
1868 | };
1869 | }
1870 |
1871 | // Analyze the overall health of the project
1872 | async getProjectHealth(projectName: string): Promise<any> {
1873 | const graph = await this.loadGraph();
1874 |
1875 | // Find the project
1876 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
1877 | if (!project) {
1878 | throw new Error(`Project '${projectName}' not found`);
1879 | }
1880 |
1881 | // Get project information
1882 | const status = project.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'planning';
1883 | const startDate = project.observations.find(o => o.startsWith('StartDate:'))?.split(':', 2)[1]?.trim();
1884 | const endDate = project.observations.find(o => o.startsWith('EndDate:'))?.split(':', 2)[1]?.trim();
1885 |
1886 | // Helper function to get entities of a specific type for this project
1887 | const getProjectEntities = (entityType: EntityType) => {
1888 | return graph.entities.filter(e =>
1889 | e.entityType === entityType &&
1890 | graph.relations.some(r => r.from === e.name && r.to === projectName && r.relationType === 'part_of')
1891 | );
1892 | };
1893 |
1894 | // Get counts of various entities
1895 | const tasks = getProjectEntities('task');
1896 | const milestones = getProjectEntities('milestone');
1897 | const issues = getProjectEntities('issue');
1898 | const risks = getProjectEntities('risk');
1899 |
1900 | // Calculate task metrics
1901 | const completedTasks = tasks.filter(t =>
1902 | t.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'completed'
1903 | ).length;
1904 |
1905 | const blockedTasks = tasks.filter(t =>
1906 | t.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'blocked'
1907 | ).length;
1908 |
1909 | const taskCompletionRate = tasks.length > 0 ? (completedTasks / tasks.length) * 100 : 0;
1910 |
1911 | // Calculate milestone metrics
1912 | const reachedMilestones = milestones.filter(m =>
1913 | m.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'reached'
1914 | ).length;
1915 |
1916 | const missedMilestones = milestones.filter(m =>
1917 | m.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'missed'
1918 | ).length;
1919 |
1920 | const milestoneCompletionRate = milestones.length > 0 ? (reachedMilestones / milestones.length) * 100 : 0;
1921 |
1922 | // Calculate issue metrics
1923 | const resolvedIssues = issues.filter(i =>
1924 | i.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'resolved'
1925 | ).length;
1926 |
1927 | const openIssues = issues.length - resolvedIssues;
1928 |
1929 | // Calculate risk metrics
1930 | const mitigatedRisks = risks.filter(r =>
1931 | r.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'mitigating' ||
1932 | r.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'avoided'
1933 | ).length;
1934 |
1935 | const activeRisks = risks.filter(r =>
1936 | r.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'identified' ||
1937 | r.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() === 'monitoring'
1938 | ).length;
1939 |
1940 | // Calculate timeline metrics
1941 | let timelineProgress = 0;
1942 | let behindSchedule = false;
1943 |
1944 | if (startDate && endDate) {
1945 | const start = new Date(startDate).getTime();
1946 | const end = new Date(endDate).getTime();
1947 | const now = new Date().getTime();
1948 |
1949 | if (end > start) {
1950 | // Calculate percentage of timeline elapsed
1951 | const totalDuration = end - start;
1952 | const elapsed = now - start;
1953 | const timeElapsedPercent = Math.min(100, Math.max(0, (elapsed / totalDuration) * 100));
1954 |
1955 | // Calculate if project is behind schedule (completion percentage significantly less than time elapsed)
1956 | behindSchedule = taskCompletionRate < (timeElapsedPercent - 15); // More than 15% behind
1957 |
1958 | timelineProgress = Math.round(timeElapsedPercent);
1959 | }
1960 | }
1961 |
1962 | // Calculate overall health score
1963 | // This is a simple formula - can be adjusted based on specific project needs
1964 | const healthFactors = [
1965 | // Task factors
1966 | tasks.length > 0 ? Math.min(100, taskCompletionRate) : 50,
1967 | tasks.length > 0 ? Math.max(0, 100 - (blockedTasks / tasks.length) * 200) : 50,
1968 |
1969 | // Milestone factors
1970 | milestones.length > 0 ? Math.min(100, milestoneCompletionRate) : 50,
1971 | milestones.length > 0 ? Math.max(0, 100 - (missedMilestones / milestones.length) * 200) : 50,
1972 |
1973 | // Issue factors
1974 | issues.length > 0 ? Math.min(100, (resolvedIssues / issues.length) * 100) : 50,
1975 | issues.length > 0 ? Math.max(0, 100 - (openIssues / issues.length) * 100) : 50,
1976 |
1977 | // Risk factors
1978 | risks.length > 0 ? Math.min(100, (mitigatedRisks / risks.length) * 100) : 50,
1979 | risks.length > 0 ? Math.max(0, 100 - (activeRisks / risks.length) * 100) : 50,
1980 |
1981 | // Schedule factor
1982 | behindSchedule ? 30 : 70 // Penalize being behind schedule
1983 | ];
1984 |
1985 | // Average the health factors
1986 | const healthScore = Math.round(healthFactors.reduce((sum, factor) => sum + factor, 0) / healthFactors.length);
1987 |
1988 | // Determine health status
1989 | let healthStatus;
1990 | if (healthScore >= 80) {
1991 | healthStatus = 'healthy';
1992 | } else if (healthScore >= 60) {
1993 | healthStatus = 'attention_needed';
1994 | } else if (healthScore >= 40) {
1995 | healthStatus = 'at_risk';
1996 | } else {
1997 | healthStatus = 'critical';
1998 | }
1999 |
2000 | // Find top issues (if any)
2001 | const topIssues = issues
2002 | .filter(i =>
2003 | i.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() !== 'resolved' &&
2004 | i.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() !== 'wont_fix'
2005 | )
2006 | .sort((a, b) => {
2007 | const aPriority = a.observations.find(o => o.startsWith('Priority:'))?.split(':', 2)[1]?.trim() || 'N/A';
2008 | const bPriority = b.observations.find(o => o.startsWith('Priority:'))?.split(':', 2)[1]?.trim() || 'N/A';
2009 |
2010 | // Simple priority sorting
2011 | if (aPriority === 'high' && bPriority !== 'high') return -1;
2012 | if (aPriority !== 'high' && bPriority === 'high') return 1;
2013 | if (aPriority === 'N/A' && bPriority === 'low') return -1;
2014 | if (aPriority === 'low' && bPriority === 'N/A') return 1;
2015 | return 0;
2016 | })
2017 | .slice(0, 3); // Top 3 issues
2018 |
2019 | return {
2020 | project,
2021 | healthScore,
2022 | healthStatus,
2023 | metrics: {
2024 | tasks: {
2025 | total: tasks.length,
2026 | completed: completedTasks,
2027 | blocked: blockedTasks,
2028 | completionRate: Math.round(taskCompletionRate)
2029 | },
2030 | milestones: {
2031 | total: milestones.length,
2032 | reached: reachedMilestones,
2033 | missed: missedMilestones,
2034 | completionRate: Math.round(milestoneCompletionRate)
2035 | },
2036 | issues: {
2037 | total: issues.length,
2038 | resolved: resolvedIssues,
2039 | open: openIssues,
2040 | resolutionRate: issues.length > 0 ? Math.round((resolvedIssues / issues.length) * 100) : 0
2041 | },
2042 | risks: {
2043 | total: risks.length,
2044 | mitigated: mitigatedRisks,
2045 | active: activeRisks,
2046 | mitigationRate: risks.length > 0 ? Math.round((mitigatedRisks / risks.length) * 100) : 0
2047 | },
2048 | timeline: {
2049 | progress: timelineProgress,
2050 | behindSchedule
2051 | }
2052 | },
2053 | topIssues,
2054 | recommendations: this.generateHealthRecommendations(healthStatus, {
2055 | tasks,
2056 | milestones,
2057 | issues,
2058 | risks,
2059 | behindSchedule
2060 | })
2061 | };
2062 | }
2063 |
2064 | // Helper method to generate health recommendations
2065 | private generateHealthRecommendations(healthStatus: string, metrics: any): string[] {
2066 | const recommendations: string[] = [];
2067 |
2068 | switch (healthStatus) {
2069 | case 'healthy':
2070 | recommendations.push('Continue current management practices');
2071 | recommendations.push('Document successful strategies for future projects');
2072 | break;
2073 |
2074 | case 'attention_needed':
2075 | if (metrics.tasks.blocked > 0) {
2076 | recommendations.push('Address blocked tasks to maintain momentum');
2077 | }
2078 | if (metrics.issues.open > 2) {
2079 | recommendations.push('Resolve open issues to prevent escalation');
2080 | }
2081 | if (metrics.behindSchedule) {
2082 | recommendations.push('Review project timeline and adjust as needed');
2083 | }
2084 | break;
2085 |
2086 | case 'at_risk':
2087 | if (metrics.tasks.blocked > 0) {
2088 | recommendations.push('Urgently resolve blocked tasks - consider reassigning resources');
2089 | }
2090 | if (metrics.behindSchedule) {
2091 | recommendations.push('Reevaluate project scope and timeline - consider adjustments');
2092 | }
2093 | if (metrics.risks.active > 0) {
2094 | recommendations.push('Implement mitigation strategies for active risks immediately');
2095 | }
2096 | if (metrics.issues.open > 0) {
2097 | recommendations.push('Prioritize issue resolution and prevent new issues');
2098 | }
2099 | break;
2100 |
2101 | case 'critical':
2102 | recommendations.push('Conduct emergency project review with stakeholders');
2103 | recommendations.push('Consider project restructuring or reset');
2104 | recommendations.push('Implement daily status meetings and tight monitoring');
2105 | if (metrics.tasks.blocked > 0) {
2106 | recommendations.push('Escalate blocked tasks to management for immediate action');
2107 | }
2108 | if (metrics.risks.active > 0) {
2109 | recommendations.push('Reassess all project risks and implement mitigation measures');
2110 | }
2111 | break;
2112 | }
2113 |
2114 | return recommendations;
2115 | }
2116 | }
2117 |
2118 | // Setup the MCP server
2119 | async function main() {
2120 | try {
2121 | const knowledgeGraphManager = new KnowledgeGraphManager();
2122 |
2123 | // Initialize status and priority entities
2124 | await knowledgeGraphManager.initializeStatusAndPriority();
2125 |
2126 | // Create the MCP server with a name and version
2127 | const server = new McpServer({
2128 | name: "Context Manager",
2129 | version: "1.0.0"
2130 | });
2131 |
2132 | // Define a resource that exposes the entire graph
2133 | server.resource(
2134 | "graph",
2135 | "graph://project",
2136 | async (uri) => ({
2137 | contents: [{
2138 | uri: uri.href,
2139 | text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2)
2140 | }]
2141 | })
2142 | );
2143 |
2144 | /**
2145 | * Start a new work session. Returns session ID, recent sessions, active projects, high-priority tasks, upcoming milestones, and project health summary.
2146 | */
2147 | server.tool(
2148 | "startsession",
2149 | toolDescriptions["startsession"],
2150 | {},
2151 | async () => {
2152 | try {
2153 | // Generate a unique session ID
2154 | const sessionId = generateSessionId();
2155 |
2156 | // Get recent sessions from persistent storage instead of entities
2157 | const allSessionStates = await loadSessionStates();
2158 |
2159 | // Initialize the session state
2160 | allSessionStates.set(sessionId, []);
2161 | await saveSessionStates(allSessionStates);
2162 |
2163 | // Convert sessions map to array and get recent sessions
2164 | const recentSessions = Array.from(allSessionStates.entries())
2165 | .map(([id, stages]) => {
2166 | // Extract summary data from the first stage (if it exists)
2167 | const summaryStage = stages.find(s => s.stage === "summary");
2168 | return {
2169 | id,
2170 | project: summaryStage?.stageData?.project || "Unknown project",
2171 | summary: summaryStage?.stageData?.summary || "No summary available"
2172 | };
2173 | })
2174 | .slice(0, 3); // Default to 3 recent sessions
2175 |
2176 | // Get all projects
2177 | const projectsQuery = await knowledgeGraphManager.searchNodes("entityType:project");
2178 | const projects = [];
2179 |
2180 | // Filter for active projects based on has_status relation
2181 | for (const project of projectsQuery.entities) {
2182 | const status = await knowledgeGraphManager.getEntityStatus(project.name);
2183 | if (status === "active") {
2184 | projects.push(project);
2185 | }
2186 | }
2187 |
2188 | // Get tasks
2189 | const taskQuery = await knowledgeGraphManager.searchNodes("entityType:task");
2190 | const tasks = [];
2191 |
2192 | // Filter for high priority and active tasks
2193 | for (const task of taskQuery.entities) {
2194 | const status = await knowledgeGraphManager.getEntityStatus(task.name);
2195 | const priority = await knowledgeGraphManager.getEntityPriority(task.name);
2196 |
2197 | if (status === "active" && priority === "high") {
2198 | tasks.push(task);
2199 | }
2200 | }
2201 |
2202 | // Get milestones
2203 | const milestoneQuery = await knowledgeGraphManager.searchNodes("entityType:milestone");
2204 | const milestones = [];
2205 |
2206 | // Filter for upcoming milestones
2207 | for (const milestone of milestoneQuery.entities) {
2208 | const status = await knowledgeGraphManager.getEntityStatus(milestone.name);
2209 | if (status === "planned" || status === "approaching") {
2210 | milestones.push(milestone);
2211 | }
2212 | }
2213 |
2214 | // Get risks
2215 | const riskQuery = await knowledgeGraphManager.searchNodes("entityType:risk");
2216 | const risks = [];
2217 |
2218 | // Filter for high priority risks
2219 | for (const risk of riskQuery.entities) {
2220 | const priority = await knowledgeGraphManager.getEntityPriority(risk.name);
2221 | if (priority === "high") {
2222 | risks.push(risk);
2223 | }
2224 | }
2225 |
2226 | // Prepare display text with truncated previews
2227 | const projectsText = await Promise.all(projects.map(async (p) => {
2228 | const status = await knowledgeGraphManager.getEntityStatus(p.name) || "Unknown";
2229 | const priority = await knowledgeGraphManager.getEntityPriority(p.name);
2230 | const priorityText = priority ? `, Priority: ${priority}` : "";
2231 |
2232 | // Show truncated preview of first observation
2233 | const preview = p.observations.length > 0
2234 | ? `${p.observations[0].substring(0, 60)}${p.observations[0].length > 60 ? '...' : ''}`
2235 | : "No description";
2236 |
2237 | return `- **${p.name}** (Status: ${status}${priorityText}): ${preview}`;
2238 | }));
2239 |
2240 | const tasksText = await Promise.all(tasks.slice(0, 10).map(async (t) => {
2241 | const status = await knowledgeGraphManager.getEntityStatus(t.name) || "Unknown";
2242 | const priority = await knowledgeGraphManager.getEntityPriority(t.name) || "Unknown";
2243 | const projectObs = t.observations.find(o => o.startsWith("project:"));
2244 | const project = projectObs ? projectObs.substring(8) : "Unknown project";
2245 |
2246 | // Show truncated preview of first non-project observation
2247 | const nonProjectObs = t.observations.find(o => !o.startsWith("project:"));
2248 | const preview = nonProjectObs
2249 | ? `${nonProjectObs.substring(0, 60)}${nonProjectObs.length > 60 ? '...' : ''}`
2250 | : "No description";
2251 |
2252 | return `- **${t.name}** (Project: ${project}, Status: ${status}, Priority: ${priority}): ${preview}`;
2253 | }));
2254 |
2255 | const milestonesText = await Promise.all(milestones.slice(0, 8).map(async (m) => {
2256 | const status = await knowledgeGraphManager.getEntityStatus(m.name) || "Unknown";
2257 | const projectObs = m.observations.find(o => o.startsWith("project:"));
2258 | const project = projectObs ? projectObs.substring(8) : "Unknown project";
2259 |
2260 | // Show truncated preview of first non-project observation
2261 | const nonProjectObs = m.observations.find(o => !o.startsWith("project:"));
2262 | const preview = nonProjectObs
2263 | ? `${nonProjectObs.substring(0, 60)}${nonProjectObs.length > 60 ? '...' : ''}`
2264 | : "No description";
2265 |
2266 | return `- **${m.name}** (Project: ${project}, Status: ${status}): ${preview}`;
2267 | }));
2268 |
2269 | const risksText = await Promise.all(risks.slice(0, 5).map(async (r) => {
2270 | const priority = await knowledgeGraphManager.getEntityPriority(r.name) || "Unknown";
2271 | const projectObs = r.observations.find(o => o.startsWith("project:"));
2272 | const project = projectObs ? projectObs.substring(8) : "Unknown project";
2273 |
2274 | // Show truncated preview of first non-project observation
2275 | const nonProjectObs = r.observations.find(o => !o.startsWith("project:"));
2276 | const preview = nonProjectObs
2277 | ? `${nonProjectObs.substring(0, 60)}${nonProjectObs.length > 60 ? '...' : ''}`
2278 | : "No description";
2279 |
2280 | return `- **${r.name}** (Project: ${project}, Priority: ${priority}): ${preview}`;
2281 | }));
2282 |
2283 | const sessionsText = recentSessions.map(s => {
2284 | return `- ${s.project} - ${s.summary.substring(0, 60)}${s.summary.length > 60 ? '...' : ''}`;
2285 | }).join("\n");
2286 |
2287 | return {
2288 | content: [{
2289 | type: "text",
2290 | text: `# Choose what to focus on in this session
2291 |
2292 | ## Session ID
2293 | \`${sessionId}\`
2294 |
2295 | ## Recent Project Management Sessions
2296 | ${sessionsText || "No recent sessions found."}
2297 |
2298 | ## Active Projects
2299 | ${projectsText.join("\n") || "No active projects found."}
2300 |
2301 | ## High-Priority Tasks
2302 | ${tasksText.join("\n") || "No high-priority tasks found."}
2303 |
2304 | ## Upcoming Milestones
2305 | ${milestonesText.join("\n") || "No upcoming milestones found."}
2306 |
2307 | ## Top Project Risks
2308 | ${risksText.join("\n") || "No high severity risks identified."}
2309 |
2310 | To load specific project context, use the \`loadcontext\` tool with the project name and session ID - ${sessionId}`
2311 | }]
2312 | };
2313 | } catch (error) {
2314 | return {
2315 | content: [{
2316 | type: "text",
2317 | text: JSON.stringify({
2318 | success: false,
2319 | error: error instanceof Error ? error.message : String(error)
2320 | }, null, 2)
2321 | }]
2322 | };
2323 | }
2324 | }
2325 | );
2326 |
2327 | /**
2328 | * Load context for a specific entity
2329 | */
2330 | server.tool(
2331 | "loadcontext",
2332 | toolDescriptions["loadcontext"],
2333 | {
2334 | entityName: z.string(),
2335 | entityType: z.string().optional(),
2336 | sessionId: z.string().optional() // Optional to maintain backward compatibility
2337 | },
2338 | async ({ entityName, entityType = "project", sessionId }) => {
2339 | try {
2340 | // Validate session if ID is provided
2341 | if (sessionId) {
2342 | const sessionStates = await loadSessionStates();
2343 | if (!sessionStates.has(sessionId)) {
2344 | console.warn(`Warning: Session ${sessionId} not found, but proceeding with context load`);
2345 | // Initialize it anyway for more robustness
2346 | sessionStates.set(sessionId, []);
2347 | await saveSessionStates(sessionStates);
2348 | }
2349 |
2350 | // Track that this entity was loaded in this session
2351 | const sessionState = sessionStates.get(sessionId) || [];
2352 | const loadEvent = {
2353 | type: 'context_loaded',
2354 | timestamp: new Date().toISOString(),
2355 | entityName,
2356 | entityType
2357 | };
2358 | sessionState.push(loadEvent);
2359 | sessionStates.set(sessionId, sessionState);
2360 | await saveSessionStates(sessionStates);
2361 | }
2362 |
2363 | // Get the entity
2364 | // Changed from using 'name:' prefix to directly searching by the entity name
2365 | const entityGraph = await knowledgeGraphManager.searchNodes(entityName);
2366 | if (entityGraph.entities.length === 0) {
2367 | throw new Error(`Entity ${entityName} not found`);
2368 | }
2369 |
2370 | // Find the exact entity by name (case-sensitive match)
2371 | const entity = entityGraph.entities.find(e => e.name === entityName);
2372 | if (!entity) {
2373 | throw new Error(`Entity ${entityName} not found`);
2374 | }
2375 |
2376 | // Different context loading based on entity type
2377 | let contextMessage = "";
2378 |
2379 | if (entityType === "project") {
2380 | // Get project overview
2381 | const projectOverview = await knowledgeGraphManager.getProjectOverview(entityName);
2382 |
2383 | // Get status and priority using relation-based approach
2384 | const status = await knowledgeGraphManager.getEntityStatus(entityName) || "Unknown";
2385 | const priority = await knowledgeGraphManager.getEntityPriority(entityName);
2386 | const priorityText = priority ? `- **Priority**: ${priority}` : "";
2387 |
2388 | // Format observations without truncation or pattern matching
2389 | const observationsList = entity.observations.length > 0
2390 | ? entity.observations.map(obs => `- ${obs}`).join("\n")
2391 | : "No observations";
2392 |
2393 | // Format tasks
2394 | const tasksText = await Promise.all((projectOverview.tasks || []).map(async (task: Entity) => {
2395 | const taskStatus = await knowledgeGraphManager.getEntityStatus(task.name) || "Unknown";
2396 | const taskPriority = await knowledgeGraphManager.getEntityPriority(task.name) || "Not set";
2397 | // Find the first observation that doesn't look like metadata
2398 | const description = task.observations.find(o =>
2399 | !o.startsWith('Project:') &&
2400 | !o.includes(':')
2401 | ) || "No description";
2402 |
2403 | return `- **${task.name}** (Status: ${taskStatus}, Priority: ${taskPriority}): ${description}`;
2404 | }));
2405 |
2406 | // Format milestones
2407 | const milestonesText = await Promise.all((projectOverview.milestones || []).map(async (milestone: Entity) => {
2408 | const milestoneStatus = await knowledgeGraphManager.getEntityStatus(milestone.name) || "Unknown";
2409 | // Find the first observation that doesn't look like metadata
2410 | const description = milestone.observations.find(o =>
2411 | !o.startsWith('Project:') &&
2412 | !o.includes(':')
2413 | ) || "No description";
2414 |
2415 | return `- **${milestone.name}** (Status: ${milestoneStatus}): ${description}`;
2416 | }));
2417 |
2418 | // Format issues
2419 | const issuesText = await Promise.all((projectOverview.issues || []).map(async (issue: Entity) => {
2420 | const issueStatus = await knowledgeGraphManager.getEntityStatus(issue.name) || "Unknown";
2421 | const issuePriority = await knowledgeGraphManager.getEntityPriority(issue.name) || "Not set";
2422 | // Find the first observation that doesn't look like metadata
2423 | const description = issue.observations.find(o =>
2424 | !o.startsWith('Project:') &&
2425 | !o.includes(':')
2426 | ) || "No description";
2427 |
2428 | return `- **${issue.name}** (Status: ${issueStatus}, Priority: ${issuePriority}): ${description}`;
2429 | }));
2430 |
2431 | // Format team members
2432 | const teamMembersText = (projectOverview.teamMembers || []).map((member: Entity) => {
2433 | const role = member.observations.find(o => o.startsWith('Role:'))?.split(':', 2)[1]?.trim() || 'Not specified';
2434 | return `- **${member.name}** (Role: ${role})`;
2435 | }).join("\n") || "No team members found";
2436 |
2437 | // Format risks
2438 | const risksText = await Promise.all((projectOverview.risks || []).map(async (risk: Entity) => {
2439 | const riskStatus = await knowledgeGraphManager.getEntityStatus(risk.name) || "Unknown";
2440 | const riskPriority = await knowledgeGraphManager.getEntityPriority(risk.name) || "Not set";
2441 | // Find the first observation that doesn't look like metadata
2442 | const description = risk.observations.find(o =>
2443 | !o.startsWith('Project:') &&
2444 | !o.includes(':')
2445 | ) || "No description";
2446 |
2447 | return `- **${risk.name}** (Status: ${riskStatus}, Priority: ${riskPriority}): ${description}`;
2448 | }));
2449 |
2450 | contextMessage = `# Project Context: ${entityName}
2451 |
2452 | ## Project Overview
2453 | - **Status**: ${status}
2454 | ${priorityText}
2455 |
2456 | ## Observations
2457 | ${observationsList}
2458 |
2459 | ## Tasks (${projectOverview.summary.completedTasks || 0}/${projectOverview.summary.taskCount || 0} completed)
2460 | ${tasksText.join("\n") || "No tasks found"}
2461 |
2462 | ## Milestones
2463 | ${milestonesText.join("\n") || "No milestones found"}
2464 |
2465 | ## Issues
2466 | ${issuesText.join("\n") || "No issues found"}
2467 |
2468 | ## Team Members
2469 | ${teamMembersText}
2470 |
2471 | ## Risks
2472 | ${risksText.join("\n") || "No risks found"}`;
2473 | }
2474 | else if (entityType === "task") {
2475 | // Get task dependencies and information
2476 | const taskDependencies = await knowledgeGraphManager.getTaskDependencies(entityName);
2477 |
2478 | // Get project name
2479 | const projectName = taskDependencies.projectName || "Unknown project";
2480 |
2481 | // Get status and priority using relation-based approach
2482 | const status = await knowledgeGraphManager.getEntityStatus(entityName) || "Unknown";
2483 | const priority = await knowledgeGraphManager.getEntityPriority(entityName) || "Not set";
2484 |
2485 | // Format observations without truncation or pattern matching
2486 | const observationsList = entity.observations.length > 0
2487 | ? entity.observations.map(obs => `- ${obs}`).join("\n")
2488 | : "No observations";
2489 |
2490 | // Get assignee if available
2491 | let assigneeText = "No assignee";
2492 | for (const relation of entityGraph.relations) {
2493 | if (relation.relationType === 'assigned_to' && relation.from === entityName) {
2494 | const teamMember = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'teamMember');
2495 | if (teamMember) {
2496 | assigneeText = teamMember.name;
2497 | break;
2498 | }
2499 | }
2500 | }
2501 |
2502 | // Get precedes/follows relations to show task sequence
2503 | const precedesRelations = entityGraph.relations.filter(r =>
2504 | r.relationType === 'precedes' && r.from === entityName
2505 | );
2506 |
2507 | const followsRelations = entityGraph.relations.filter(r =>
2508 | r.relationType === 'precedes' && r.to === entityName
2509 | );
2510 |
2511 | const precedesText = precedesRelations.length > 0
2512 | ? precedesRelations.map(r => `- **${r.to}**`).join("\n")
2513 | : "No tasks follow this task";
2514 |
2515 | const followsText = followsRelations.length > 0
2516 | ? followsRelations.map(r => `- **${r.from}**`).join("\n")
2517 | : "No tasks precede this task";
2518 |
2519 | // Process dependency information
2520 | const dependsOnTasks = [];
2521 | const dependedOnByTasks = [];
2522 |
2523 | for (const dep of taskDependencies.dependencies) {
2524 | if (dep.task.name !== entityName) {
2525 | if (dep.dependsOn.includes(entityName)) {
2526 | dependsOnTasks.push(dep.task);
2527 | }
2528 |
2529 | if (dep.dependedOnBy.includes(entityName)) {
2530 | dependedOnByTasks.push(dep.task);
2531 | }
2532 | }
2533 | }
2534 |
2535 | // Format dependencies with async status lookup
2536 | const dependsOnPromises = dependsOnTasks.map(async (task) => {
2537 | const depStatus = await knowledgeGraphManager.getEntityStatus(task.name) || "Unknown";
2538 | return `- **${task.name}** (Status: ${depStatus}): This task depends on ${entityName}`;
2539 | });
2540 |
2541 | const dependedOnByPromises = dependedOnByTasks.map(async (task) => {
2542 | const depStatus = await knowledgeGraphManager.getEntityStatus(task.name) || "Unknown";
2543 | return `- **${task.name}** (Status: ${depStatus}): ${entityName} depends on this task`;
2544 | });
2545 |
2546 | const dependsOnText = (await Promise.all(dependsOnPromises)).join("\n") || "No tasks depend on this task";
2547 | const dependedOnByText = (await Promise.all(dependedOnByPromises)).join("\n") || "This task doesn't depend on other tasks";
2548 |
2549 | // Determine if task is on critical path
2550 | const onCriticalPath = taskDependencies.criticalPath?.includes(entityName);
2551 | const criticalPathText = onCriticalPath ?
2552 | "⚠️ This task is on the critical path. Delays will impact the project timeline." :
2553 | "This task is not on the critical path.";
2554 |
2555 | contextMessage = `# Task Context: ${entityName}
2556 |
2557 | ## Task Overview
2558 | - **Project**: ${projectName}
2559 | - **Status**: ${status}
2560 | - **Priority**: ${priority}
2561 | - **Assignee**: ${assigneeText}
2562 | - **Critical Path**: ${criticalPathText}
2563 |
2564 | ## Observations
2565 | ${observationsList}
2566 |
2567 | ## Task Sequencing
2568 | ### Tasks That Follow This Task
2569 | ${precedesText}
2570 |
2571 | ### Tasks That Precede This Task
2572 | ${followsText}
2573 |
2574 | ## Task Dependencies
2575 | ### Tasks That Depend On This Task
2576 | ${dependsOnText}
2577 |
2578 | ### Tasks This Task Depends On
2579 | ${dependedOnByText}`;
2580 | }
2581 | else if (entityType === "milestone") {
2582 | // Get milestone progress
2583 | const projectName = entity.observations.find(o => o.startsWith('Project:'))?.split(':', 2)[1]?.trim();
2584 |
2585 | if (!projectName) {
2586 | throw new Error(`Project not found for milestone ${entityName}`);
2587 | }
2588 |
2589 | const milestoneProgress = await knowledgeGraphManager.getMilestoneProgress(projectName, entityName);
2590 |
2591 | if (!milestoneProgress || !milestoneProgress.milestones || milestoneProgress.milestones.length === 0) {
2592 | throw new Error(`Milestone progress data not available for ${entityName}`);
2593 | }
2594 |
2595 | // Find this milestone
2596 | const milestone = milestoneProgress.milestones.find((m: any) => m.milestone.name === entityName);
2597 |
2598 | if (!milestone) {
2599 | throw new Error(`Milestone ${entityName} not found in progress data`);
2600 | }
2601 |
2602 | // Format milestone context
2603 | const description = milestone.info.description || "No description available";
2604 | const date = milestone.info.date || "Not set";
2605 | const status = milestone.info.status || "planned";
2606 | const criteria = milestone.info.criteria || "Not specified";
2607 |
2608 | // Format tasks required for this milestone
2609 | const tasksText = milestone.relatedTasks?.map((task: Entity) => {
2610 | const taskStatus = task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'not_started';
2611 | return `- **${task.name}** (Status: ${taskStatus})`;
2612 | }).join("\n") || "No tasks found";
2613 |
2614 | // Format blockers
2615 | const blockersText = milestone.blockers?.map((task: Entity) => {
2616 | const taskStatus = task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'not_started';
2617 | return `- **${task.name}** (Status: ${taskStatus})`;
2618 | }).join("\n") || "No blocking tasks";
2619 |
2620 | contextMessage = `# Milestone Context: ${entityName}
2621 |
2622 | ## Milestone Details
2623 | - **Project**: ${projectName}
2624 | - **Status**: ${status}
2625 | - **Date**: ${date}
2626 | - **Completion Criteria**: ${criteria}
2627 | - **Description**: ${description}
2628 | - **Progress**: ${milestone.progress.completionPercentage || 0}% complete
2629 | - **Days Remaining**: ${milestone.progress.daysRemaining !== null ? milestone.progress.daysRemaining : 'Unknown'}
2630 | - **Overdue**: ${milestone.progress.isOverdue ? 'Yes' : 'No'}
2631 |
2632 | ## Required Tasks (${milestone.progress.completedTasks || 0}/${milestone.progress.totalTasks || 0} completed)
2633 | ${tasksText}
2634 |
2635 | ## Blocking Tasks
2636 | ${blockersText}`;
2637 | }
2638 | else if (entityType === "teamMember") {
2639 | // Get team member assignments
2640 | const teamMemberAssignments = await knowledgeGraphManager.getTeamMemberAssignments(entityName);
2641 |
2642 | // Format team member context
2643 | const role = teamMemberAssignments.info.role || "Not specified";
2644 | const skills = teamMemberAssignments.info.skills || "Not specified";
2645 | const availability = teamMemberAssignments.info.availability || "Not specified";
2646 |
2647 | // Format assigned tasks
2648 | const tasksText = teamMemberAssignments.assignedTasks?.map((assignment: any) => {
2649 | return `- **${assignment.task.name}** (Project: ${assignment.project?.name || 'Unassigned'}, Status: ${assignment.status}, Due: ${assignment.dueDate || 'Not set'})`;
2650 | }).join("\n") || "No tasks assigned";
2651 |
2652 | // Format projects
2653 | const projectsText = teamMemberAssignments.projects?.map((project: Entity) => {
2654 | return `- **${project.name}**`;
2655 | }).join("\n") || "Not assigned to any projects";
2656 |
2657 | // Format deadlines
2658 | const deadlinesText = teamMemberAssignments.upcomingDeadlines?.map((assignment: any) => {
2659 | return `- **${assignment.task.name}** (Due: ${assignment.dueDate})`;
2660 | }).join("\n") || "No upcoming deadlines";
2661 |
2662 | // Format overdue tasks
2663 | const overdueText = teamMemberAssignments.overdueTasks?.map((assignment: any) => {
2664 | return `- **${assignment.task.name}** (Due: ${assignment.dueDate})`;
2665 | }).join("\n") || "No overdue tasks";
2666 |
2667 | contextMessage = `# Team Member Context: ${entityName}
2668 |
2669 | ## Team Member Details
2670 | - **Role**: ${role}
2671 | - **Skills**: ${skills}
2672 | - **Availability**: ${availability}
2673 | - **Workload**: ${teamMemberAssignments.assignedTasks.length} tasks assigned (${teamMemberAssignments.workload.completionRate}% completed)
2674 |
2675 | ## Assigned Tasks
2676 | ${tasksText}
2677 |
2678 | ## Projects
2679 | ${projectsText}
2680 |
2681 | ## Upcoming Deadlines
2682 | ${deadlinesText}
2683 |
2684 | ## Overdue Tasks
2685 | ${overdueText}`;
2686 | }
2687 | else if (entityType === "resource") {
2688 | // Find which project this resource belongs to
2689 | let projectName = 'Unknown project';
2690 |
2691 | for (const relation of entityGraph.relations) {
2692 | if (relation.relationType === 'part_of' && relation.from === entityName) {
2693 | const project = entityGraph.entities.find(e => e.name === relation.to && e.entityType === 'project');
2694 | if (project) {
2695 | projectName = project.name;
2696 | break;
2697 | }
2698 | }
2699 | }
2700 |
2701 | // Get resource allocation
2702 | const resourceAllocation = await knowledgeGraphManager.getResourceAllocation(projectName, entityName);
2703 |
2704 | if (!resourceAllocation || !resourceAllocation.resources || resourceAllocation.resources.length === 0) {
2705 | throw new Error(`Resource allocation data not available for ${entityName}`);
2706 | }
2707 |
2708 | // Find this resource
2709 | const resource = resourceAllocation.resources.find((r: any) => r.resource.name === entityName);
2710 |
2711 | if (!resource) {
2712 | throw new Error(`Resource ${entityName} not found in allocation data`);
2713 | }
2714 |
2715 | // Format resource context
2716 | const type = resource.info.type || "Not specified";
2717 | const availability = resource.info.availability || "Not specified";
2718 | const capacity = resource.info.capacity || "Not specified";
2719 | const cost = resource.info.cost || "Not specified";
2720 |
2721 | // Format assigned tasks
2722 | const tasksText = resource.assignedTasks?.map((task: Entity) => {
2723 | const status = task.observations.find(o => o.startsWith('Status:'))?.split(':', 2)[1]?.trim() || 'not_started';
2724 | return `- **${task.name}** (Status: ${status})`;
2725 | }).join("\n") || "No tasks assigned";
2726 |
2727 | // Format team members using this resource
2728 | const teamMembersText = resource.teamMembers?.map((member: Entity) => {
2729 | return `- **${member.name}**`;
2730 | }).join("\n") || "No team members assigned";
2731 |
2732 | contextMessage = `# Resource Context: ${entityName}
2733 |
2734 | ## Resource Details
2735 | - **Type**: ${type}
2736 | - **Project**: ${projectName}
2737 | - **Availability**: ${availability}
2738 | - **Capacity**: ${capacity}
2739 | - **Cost**: ${cost}
2740 | - **Usage**: ${resource.usage.usagePercentage}% (${resource.usage.inProgressTasks} tasks in progress)
2741 |
2742 | ## Assigned Tasks
2743 | ${tasksText}
2744 |
2745 | ## Team Members Using This Resource
2746 | ${teamMembersText}`;
2747 | }
2748 | else {
2749 | // Generic entity context for other entity types
2750 | // Find all relations involving this entity
2751 | const relations = await knowledgeGraphManager.openNodes([entityName]);
2752 |
2753 | // Build a text representation of related entities
2754 | const incomingRelations = relations.relations.filter(r => r.to === entityName);
2755 | const outgoingRelations = relations.relations.filter(r => r.from === entityName);
2756 |
2757 | const incomingText = incomingRelations.map(rel => {
2758 | const sourceEntity = relations.entities.find(e => e.name === rel.from);
2759 | if (!sourceEntity) return null;
2760 | return `- **${sourceEntity.name}** (${sourceEntity.entityType}) → ${rel.relationType} → ${entityName}`;
2761 | }).filter(Boolean).join("\n") || "No incoming relations";
2762 |
2763 | const outgoingText = outgoingRelations.map(rel => {
2764 | const targetEntity = relations.entities.find(e => e.name === rel.to);
2765 | if (!targetEntity) return null;
2766 | return `- **${entityName}** → ${rel.relationType} → **${targetEntity.name}** (${targetEntity.entityType})`;
2767 | }).filter(Boolean).join("\n") || "No outgoing relations";
2768 |
2769 | // Format observations
2770 | const observationsText = entity.observations.map((obs: string) => `- ${obs}`).join("\n") || "No observations";
2771 |
2772 | contextMessage = `# Entity Context: ${entityName} (${entityType})
2773 |
2774 | ## Observations
2775 | ${observationsText}
2776 |
2777 | ## Incoming Relations
2778 | ${incomingText}
2779 |
2780 | ## Outgoing Relations
2781 | ${outgoingText}`;
2782 | }
2783 |
2784 | return {
2785 | content: [{
2786 | type: "text",
2787 | text: contextMessage
2788 | }]
2789 | };
2790 | } catch (error) {
2791 | return {
2792 | content: [{
2793 | type: "text",
2794 | text: JSON.stringify({
2795 | success: false,
2796 | error: error instanceof Error ? error.message : String(error)
2797 | }, null, 2)
2798 | }]
2799 | };
2800 | }
2801 | }
2802 | );
2803 |
2804 | // Helper function to process each stage of endsession
2805 | async function processStage(params: {
2806 | sessionId: string;
2807 | stage: string;
2808 | stageNumber: number;
2809 | totalStages: number;
2810 | analysis?: string;
2811 | stageData?: any;
2812 | nextStageNeeded: boolean;
2813 | isRevision?: boolean;
2814 | revisesStage?: number;
2815 | }, previousStages: any[]): Promise<any> {
2816 | // Process based on the stage
2817 | switch (params.stage) {
2818 | case "summary":
2819 | // Process summary stage
2820 | return {
2821 | stage: "summary",
2822 | stageNumber: params.stageNumber,
2823 | analysis: params.analysis || "",
2824 | stageData: params.stageData || {
2825 | summary: "",
2826 | duration: "",
2827 | project: ""
2828 | },
2829 | completed: !params.nextStageNeeded
2830 | };
2831 |
2832 | case "achievements":
2833 | // Process achievements stage
2834 | return {
2835 | stage: "achievements",
2836 | stageNumber: params.stageNumber,
2837 | analysis: params.analysis || "",
2838 | stageData: params.stageData || { achievements: [] },
2839 | completed: !params.nextStageNeeded
2840 | };
2841 |
2842 | case "taskUpdates":
2843 | // Process task updates stage
2844 | return {
2845 | stage: "taskUpdates",
2846 | stageNumber: params.stageNumber,
2847 | analysis: params.analysis || "",
2848 | stageData: params.stageData || { updates: [] },
2849 | completed: !params.nextStageNeeded
2850 | };
2851 |
2852 | case "newTasks":
2853 | // Process new tasks stage
2854 | return {
2855 | stage: "newTasks",
2856 | stageNumber: params.stageNumber,
2857 | analysis: params.analysis || "",
2858 | stageData: params.stageData || { tasks: [] },
2859 | completed: !params.nextStageNeeded
2860 | };
2861 |
2862 | case "projectStatus":
2863 | // Process project status stage
2864 | return {
2865 | stage: "projectStatus",
2866 | stageNumber: params.stageNumber,
2867 | analysis: params.analysis || "",
2868 | stageData: params.stageData || {
2869 | projectStatus: "",
2870 | projectObservation: ""
2871 | },
2872 | completed: !params.nextStageNeeded
2873 | };
2874 |
2875 | case "riskUpdates":
2876 | // Process risk updates stage
2877 | return {
2878 | stage: "riskUpdates",
2879 | stageNumber: params.stageNumber,
2880 | analysis: params.analysis || "",
2881 | stageData: params.stageData || { risks: [] },
2882 | completed: !params.nextStageNeeded
2883 | };
2884 |
2885 | case "assembly":
2886 | // Final assembly stage - compile all arguments for end-session
2887 | return {
2888 | stage: "assembly",
2889 | stageNumber: params.stageNumber,
2890 | analysis: "Final assembly of end-session arguments",
2891 | stageData: assembleEndSessionArgs(previousStages),
2892 | completed: true
2893 | };
2894 |
2895 | default:
2896 | throw new Error(`Unknown stage: ${params.stage}`);
2897 | }
2898 | }
2899 |
2900 | // Helper function to assemble the final end-session arguments
2901 | function assembleEndSessionArgs(stages: any[]): any {
2902 | const summaryStage = stages.find(s => s.stage === "summary");
2903 | const achievementsStage = stages.find(s => s.stage === "achievements");
2904 | const taskUpdatesStage = stages.find(s => s.stage === "taskUpdates");
2905 | const newTasksStage = stages.find(s => s.stage === "newTasks");
2906 | const projectStatusStage = stages.find(s => s.stage === "projectStatus");
2907 | const riskUpdatesStage = stages.find(s => s.stage === "riskUpdates");
2908 |
2909 | return {
2910 | summary: summaryStage?.stageData?.summary || "",
2911 | duration: summaryStage?.stageData?.duration || "unknown",
2912 | project: summaryStage?.stageData?.project || "",
2913 | achievements: JSON.stringify(achievementsStage?.stageData?.achievements || []),
2914 | taskUpdates: JSON.stringify(taskUpdatesStage?.stageData?.updates || []),
2915 | projectStatus: projectStatusStage?.stageData?.projectStatus || "",
2916 | projectObservation: projectStatusStage?.stageData?.projectObservation || "",
2917 | newTasks: JSON.stringify(newTasksStage?.stageData?.tasks || []),
2918 | riskUpdates: JSON.stringify(riskUpdatesStage?.stageData?.risks || [])
2919 | };
2920 | }
2921 |
2922 | /**
2923 | * End session by processing all stages and recording the final results.
2924 | * Only use this tool if the user asks for it.
2925 | *
2926 | * Usage examples:
2927 | *
2928 | * 1. Starting the end session process with the summary stage:
2929 | * {
2930 | * "sessionId": "proj_1234567890_abc123", // From startsession
2931 | * "stage": "summary",
2932 | * "stageNumber": 1,
2933 | * "totalStages": 6,
2934 | * "analysis": "Analyzed progress on the marketing campaign project",
2935 | * "stageData": {
2936 | * "summary": "Completed the social media strategy components",
2937 | * "duration": "4 hours",
2938 | * "project": "Q4 Marketing Campaign" // Project name
2939 | * },
2940 | * "nextStageNeeded": true, // More stages coming
2941 | * "isRevision": false
2942 | * }
2943 | *
2944 | * 2. Middle stage for milestones:
2945 | * {
2946 | * "sessionId": "proj_1234567890_abc123",
2947 | * "stage": "milestones",
2948 | * "stageNumber": 2,
2949 | * "totalStages": 6,
2950 | * "analysis": "Updated milestone progress",
2951 | * "stageData": {
2952 | * "milestones": [
2953 | * { "name": "Content Creation", "status": "completed", "notes": "All blog posts and social media content finished" },
2954 | * { "name": "Channel Selection", "status": "in_progress", "notes": "Evaluating performance of different platforms" }
2955 | * ]
2956 | * },
2957 | * "nextStageNeeded": true,
2958 | * "isRevision": false
2959 | * }
2960 | *
2961 | * 3. Final assembly stage:
2962 | * {
2963 | * "sessionId": "proj_1234567890_abc123",
2964 | * "stage": "assembly",
2965 | * "stageNumber": 6,
2966 | * "totalStages": 6,
2967 | * "nextStageNeeded": false, // This completes the session
2968 | * "isRevision": false
2969 | * }
2970 | */
2971 | server.tool(
2972 | "endsession",
2973 | toolDescriptions["endsession"],
2974 | {
2975 | sessionId: z.string().describe("The unique session identifier obtained from startsession"),
2976 | stage: z.string().describe("Current stage of analysis: 'summary', 'milestones', 'risks', 'tasks', 'teamUpdates', or 'assembly'"),
2977 | stageNumber: z.number().int().positive().describe("The sequence number of the current stage (starts at 1)"),
2978 | totalStages: z.number().int().positive().describe("Total number of stages in the workflow (typically 6 for standard workflow)"),
2979 | analysis: z.string().optional().describe("Text analysis or observations for the current stage"),
2980 | stageData: z.record(z.string(), z.any()).optional().describe(`Stage-specific data structure - format depends on the stage type:
2981 | - For 'summary' stage: { summary: "Session summary text", duration: "4 hours", project: "Project Name" }
2982 | - For 'milestones' stage: { milestones: [{ name: "Milestone1", status: "completed", notes: "Notes about completion" }] }
2983 | - For 'risks' stage: { risks: [{ name: "Risk1", severity: "high", mitigation: "Plan to address this risk" }] }
2984 | - For 'tasks' stage: { tasks: [{ name: "Task1", status: "in_progress", assignee: "Team Member", notes: "Status update" }] }
2985 | - For 'teamUpdates' stage: { teamUpdates: [{ member: "Team Member", status: "Completed assigned tasks", blockers: "None" }] }
2986 | - For 'assembly' stage: no stageData needed - automatic assembly of previous stages`),
2987 | nextStageNeeded: z.boolean().describe("Whether additional stages are needed after this one (false for final stage)"),
2988 | isRevision: z.boolean().optional().describe("Whether this is revising a previous stage"),
2989 | revisesStage: z.number().int().positive().optional().describe("If revising, which stage number is being revised")
2990 | },
2991 | async (params, extra) => {
2992 | try {
2993 | // Load session states from persistent storage
2994 | const sessionStates = await loadSessionStates();
2995 |
2996 | // Validate session ID
2997 | if (!sessionStates.has(params.sessionId)) {
2998 | return {
2999 | content: [{
3000 | type: "text",
3001 | text: JSON.stringify({
3002 | success: false,
3003 | error: `Session with ID ${params.sessionId} not found. Please start a new session with startsession.`
3004 | }, null, 2)
3005 | }]
3006 | };
3007 | }
3008 |
3009 | // Get or initialize session state
3010 | let sessionState = sessionStates.get(params.sessionId) || [];
3011 |
3012 | // Process the current stage
3013 | const stageResult = await processStage(params, sessionState);
3014 |
3015 | // Store updated state
3016 | if (params.isRevision && params.revisesStage) {
3017 | // Find the analysis stages in the session state
3018 | const analysisStages = sessionState.filter(item => item.type === 'analysis_stage') || [];
3019 |
3020 | if (params.revisesStage <= analysisStages.length) {
3021 | // Replace the revised stage
3022 | analysisStages[params.revisesStage - 1] = {
3023 | type: 'analysis_stage',
3024 | ...stageResult
3025 | };
3026 | } else {
3027 | // Add as a new stage
3028 | analysisStages.push({
3029 | type: 'analysis_stage',
3030 | ...stageResult
3031 | });
3032 | }
3033 |
3034 | // Update the session state with the modified analysis stages
3035 | sessionState = [
3036 | ...sessionState.filter(item => item.type !== 'analysis_stage'),
3037 | ...analysisStages
3038 | ];
3039 | } else {
3040 | // Add new stage
3041 | sessionState.push({
3042 | type: 'analysis_stage',
3043 | ...stageResult
3044 | });
3045 | }
3046 |
3047 | // Update in persistent storage
3048 | sessionStates.set(params.sessionId, sessionState);
3049 | await saveSessionStates(sessionStates);
3050 |
3051 | // Check if this is the final assembly stage and no more stages are needed
3052 | if (params.stage === "assembly" && !params.nextStageNeeded) {
3053 | // Get the assembled arguments
3054 | const args = stageResult.stageData;
3055 |
3056 | try {
3057 | // Parse arguments
3058 | const summary = args.summary;
3059 | const duration = args.duration;
3060 | const project = args.project;
3061 | const achievements = args.achievements ? JSON.parse(args.achievements) : [];
3062 | const taskUpdates = args.taskUpdates ? JSON.parse(args.taskUpdates) : [];
3063 | const projectStatus = args.projectStatus;
3064 | const projectObservation = args.projectObservation;
3065 | const newTasks = args.newTasks ? JSON.parse(args.newTasks) : [];
3066 | const riskUpdates = args.riskUpdates ? JSON.parse(args.riskUpdates) : [];
3067 |
3068 | // Create a timestamp to use for entity naming
3069 | const timestamp = new Date().getTime().toString();
3070 |
3071 | // Create achievement entities and link them to the project
3072 | const achievementEntities = await Promise.all(achievements.map(async (achievement: string, index: number) => {
3073 | const achievementName = `achievement_${timestamp}_${index}`;
3074 | await knowledgeGraphManager.createEntities([{
3075 | name: achievementName,
3076 | entityType: 'decision',
3077 | observations: [achievement],
3078 | embedding: undefined
3079 | }]);
3080 |
3081 | await knowledgeGraphManager.createRelations([{
3082 | from: achievementName,
3083 | to: project,
3084 | relationType: 'part_of',
3085 | observations: []
3086 | }]);
3087 |
3088 | return achievementName;
3089 | }));
3090 |
3091 | // Update task statuses using entity-relation approach
3092 | await Promise.all(taskUpdates.map(async (taskUpdate: {name: string, status: string, progress?: string}) => {
3093 | try {
3094 | // Map task status to standard values
3095 | let standardStatus = taskUpdate.status;
3096 | if (taskUpdate.status === 'completed' || taskUpdate.status === 'done' || taskUpdate.status === 'finished') {
3097 | standardStatus = 'completed';
3098 | } else if (taskUpdate.status === 'in_progress' || taskUpdate.status === 'ongoing' || taskUpdate.status === 'started') {
3099 | standardStatus = 'active';
3100 | } else if (taskUpdate.status === 'not_started' || taskUpdate.status === 'planned' || taskUpdate.status === 'upcoming') {
3101 | standardStatus = 'inactive';
3102 | }
3103 |
3104 | // Update the task status using the entity-relation approach
3105 | await knowledgeGraphManager.setEntityStatus(taskUpdate.name, standardStatus);
3106 |
3107 | // If the task is completed, link it to the current session
3108 | if (standardStatus === 'completed') {
3109 | await knowledgeGraphManager.createRelations([{
3110 | from: params.sessionId,
3111 | to: taskUpdate.name,
3112 | relationType: 'resolves',
3113 | observations: []
3114 | }]);
3115 | }
3116 |
3117 | // Add progress as an observation if provided
3118 | if (taskUpdate.progress) {
3119 | await knowledgeGraphManager.addObservations(taskUpdate.name, [`Progress: ${taskUpdate.progress}`]);
3120 | }
3121 | } catch (error) {
3122 | console.error(`Error updating task ${taskUpdate.name}: ${error}`);
3123 | }
3124 | }));
3125 |
3126 | // Update project status if specified
3127 | if (project && projectStatus) {
3128 | try {
3129 | // Map project status to standard values
3130 | let standardStatus = projectStatus;
3131 | if (projectStatus === 'completed' || projectStatus === 'done' || projectStatus === 'finished') {
3132 | standardStatus = 'completed';
3133 | } else if (projectStatus === 'in_progress' || projectStatus === 'ongoing' || projectStatus === 'active') {
3134 | standardStatus = 'active';
3135 | } else if (projectStatus === 'not_started' || projectStatus === 'planned' || projectStatus === 'upcoming') {
3136 | standardStatus = 'inactive';
3137 | }
3138 |
3139 | // Update the project status using the entity-relation approach
3140 | await knowledgeGraphManager.setEntityStatus(project, standardStatus);
3141 |
3142 | // Add project observation if provided
3143 | if (projectObservation) {
3144 | await knowledgeGraphManager.addObservations(project, [projectObservation]);
3145 | }
3146 | } catch (error) {
3147 | console.error(`Error updating project ${project}: ${error}`);
3148 | }
3149 | }
3150 |
3151 | // Create new tasks with specified attributes
3152 | const newTaskEntities = await Promise.all(newTasks.map(async (task: {name: string, description: string, priority: string, precedes?: string, follows?: string}) => {
3153 | try {
3154 | // Create the task entity
3155 | await knowledgeGraphManager.createEntities([{
3156 | name: task.name,
3157 | entityType: 'task',
3158 | observations: [
3159 | task.description ? `Description: ${task.description}` : 'No description'
3160 | ],
3161 | embedding: undefined
3162 | }]);
3163 |
3164 | // Set task priority using entity-relation approach
3165 | const priority = task.priority || 'N/A';
3166 | await knowledgeGraphManager.setEntityPriority(task.name, priority);
3167 |
3168 | // Set task status to active by default using entity-relation approach
3169 | await knowledgeGraphManager.setEntityStatus(task.name, 'active');
3170 |
3171 | // Link the task to the project
3172 | await knowledgeGraphManager.createRelations([{
3173 | from: task.name,
3174 | to: project,
3175 | relationType: 'part_of',
3176 | observations: []
3177 | }]);
3178 |
3179 | // Handle task sequencing if specified
3180 | if (task.precedes) {
3181 | await knowledgeGraphManager.createRelations([{
3182 | from: task.name,
3183 | to: task.precedes,
3184 | relationType: 'precedes',
3185 | observations: []
3186 | }]);
3187 | }
3188 |
3189 | if (task.follows) {
3190 | await knowledgeGraphManager.createRelations([{
3191 | from: task.follows,
3192 | to: task.name,
3193 | relationType: 'precedes',
3194 | observations: []
3195 | }]);
3196 | }
3197 |
3198 | return task.name;
3199 | } catch (error) {
3200 | console.error(`Error creating task ${task.name}: ${error}`);
3201 | return null;
3202 | }
3203 | }));
3204 |
3205 | // Process risk updates
3206 | await Promise.all(riskUpdates.map(async (risk: {name: string, status: string, impact: string, probability: string}) => {
3207 | try {
3208 | // Try to find the risk entity, create it if it doesn't exist
3209 | const riskEntity = (await knowledgeGraphManager.openNodes([risk.name])).entities
3210 | .find(e => e.name === risk.name && e.entityType === 'risk');
3211 |
3212 | if (!riskEntity) {
3213 | // Create new risk entity
3214 | await knowledgeGraphManager.createEntities([{
3215 | name: risk.name,
3216 | entityType: 'risk',
3217 | observations: [],
3218 | embedding: undefined
3219 | }]);
3220 |
3221 | // Link it to the project
3222 | await knowledgeGraphManager.createRelations([{
3223 | from: risk.name,
3224 | to: project,
3225 | relationType: 'part_of',
3226 | observations: []
3227 | }]);
3228 | }
3229 |
3230 | // Update risk status using entity-relation approach
3231 | await knowledgeGraphManager.setEntityStatus(risk.name, risk.status);
3232 |
3233 | // Add risk observation if provided
3234 | if (risk.impact) {
3235 | await knowledgeGraphManager.addObservations(risk.name, [`Impact: ${risk.impact}`, `Probability: ${risk.probability}`]);
3236 | }
3237 | } catch (error) {
3238 | console.error(`Error updating risk ${risk.name}: ${error}`);
3239 | }
3240 | }));
3241 |
3242 | // Record session completion in persistent storage
3243 | sessionState.push({
3244 | type: 'session_completed',
3245 | timestamp: new Date().toISOString(),
3246 | summary: summary,
3247 | project: project
3248 | });
3249 |
3250 | sessionStates.set(params.sessionId, sessionState);
3251 | await saveSessionStates(sessionStates);
3252 |
3253 | // Prepare the summary message
3254 | const summaryMessage = `# Project Session Recorded
3255 |
3256 | I've recorded your project session focusing on ${project}.
3257 |
3258 | ## Decisions Documented
3259 | ${achievements.map((a: string) => `- ${a}`).join('\n') || "No decisions recorded."}
3260 |
3261 | ## Task Updates
3262 | ${taskUpdates.map((t: {name: string, status: string, progress?: string}) =>
3263 | `- ${t.name}: ${t.status}${t.progress ? ` (Progress: ${t.progress})` : ''}`
3264 | ).join('\n') || "No task updates."}
3265 |
3266 | ## Project Status
3267 | Project ${project} has been updated to: ${projectStatus}
3268 |
3269 | ${newTasks && newTasks.length > 0 ? `## New Tasks Added
3270 | ${newTasks.map((t: {name: string, description: string, priority: string}) =>
3271 | `- ${t.name}: ${t.description} (Priority: ${t.priority || "N/A"})`
3272 | ).join('\n')}` : "No new tasks added."}
3273 |
3274 | ${riskUpdates && riskUpdates.length > 0 ? `## Risk Updates
3275 | ${riskUpdates.map((r: {name: string, status: string, impact: string, probability: string}) =>
3276 | `- ${r.name}: Status ${r.status} (Impact: ${r.impact}, Probability: ${r.probability})`
3277 | ).join('\n')}` : "No risk updates."}
3278 |
3279 | ## Session Summary
3280 | ${summary}
3281 |
3282 | Would you like me to perform any additional updates to your project knowledge graph?`;
3283 |
3284 | // Return the final result with the session recorded message
3285 | return {
3286 | content: [{
3287 | type: "text",
3288 | text: JSON.stringify({
3289 | success: true,
3290 | stageCompleted: params.stage,
3291 | nextStageNeeded: false,
3292 | stageResult: stageResult,
3293 | sessionRecorded: true,
3294 | summaryMessage: summaryMessage
3295 | }, null, 2)
3296 | }]
3297 | };
3298 | } catch (error) {
3299 | return {
3300 | content: [{
3301 | type: "text",
3302 | text: JSON.stringify({
3303 | success: false,
3304 | error: `Error recording project session: ${error instanceof Error ? error.message : String(error)}`
3305 | }, null, 2)
3306 | }]
3307 | };
3308 | }
3309 | } else {
3310 | // This is not the final stage or more stages are needed
3311 | // Return intermediate result
3312 | return {
3313 | content: [{
3314 | type: "text",
3315 | text: JSON.stringify({
3316 | success: true,
3317 | stageCompleted: params.stage,
3318 | nextStageNeeded: params.nextStageNeeded,
3319 | stageResult: stageResult,
3320 | endSessionArgs: params.stage === "assembly" ? stageResult.stageData : null
3321 | }, null, 2)
3322 | }]
3323 | };
3324 | }
3325 | } catch (error) {
3326 | return {
3327 | content: [{
3328 | type: "text",
3329 | text: JSON.stringify({
3330 | success: false,
3331 | error: error instanceof Error ? error.message : String(error)
3332 | }, null, 2)
3333 | }]
3334 | };
3335 | }
3336 | }
3337 | );
3338 |
3339 |
3340 |
3341 | /**
3342 | * Create entities, relations, and observations.
3343 | */
3344 | server.tool(
3345 | "buildcontext",
3346 | toolDescriptions["buildcontext"],
3347 | {
3348 | type: z.enum(["entities", "relations", "observations"]).describe("Type of creation operation: 'entities', 'relations', or 'observations'"),
3349 | data: z.array(z.any()).describe("Data for the creation operation, structure varies by type but must be an array")
3350 | },
3351 | async ({ type, data }) => {
3352 | try {
3353 | let result;
3354 |
3355 | switch (type) {
3356 | case "entities":
3357 | // Ensure entities match the Entity interface
3358 | const typedEntities: Entity[] = data.map((e: any) => ({
3359 | name: e.name,
3360 | entityType: e.entityType,
3361 | observations: e.observations,
3362 | embedding: e.embedding
3363 | }));
3364 | result = await knowledgeGraphManager.createEntities(typedEntities);
3365 | return {
3366 | content: [{
3367 | type: "text",
3368 | text: JSON.stringify({ success: true, created: result }, null, 2)
3369 | }]
3370 | };
3371 |
3372 | case "relations":
3373 | // Ensure relations match the Relation interface
3374 | const typedRelations: Relation[] = data.map((r: any) => ({
3375 | from: r.from,
3376 | to: r.to,
3377 | relationType: r.relationType,
3378 | observations: r.observations
3379 | }));
3380 | result = await knowledgeGraphManager.createRelations(typedRelations);
3381 | return {
3382 | content: [{
3383 | type: "text",
3384 | text: JSON.stringify({ success: true, created: result }, null, 2)
3385 | }]
3386 | };
3387 |
3388 | case "observations":
3389 | // For project domain, addObservations takes entity name and observations
3390 | for (const item of data) {
3391 | if (item.entityName && Array.isArray(item.contents)) {
3392 | await knowledgeGraphManager.addObservations(item.entityName, item.contents);
3393 | }
3394 | }
3395 | return {
3396 | content: [{
3397 | type: "text",
3398 | text: JSON.stringify({ success: true, message: "Added observations to entities" }, null, 2)
3399 | }]
3400 | };
3401 |
3402 | default:
3403 | throw new Error(`Invalid type: ${type}. Must be 'entities', 'relations', or 'observations'.`);
3404 | }
3405 | } catch (error) {
3406 | return {
3407 | content: [{
3408 | type: "text",
3409 | text: JSON.stringify({
3410 | success: false,
3411 | error: error instanceof Error ? error.message : String(error)
3412 | }, null, 2)
3413 | }]
3414 | };
3415 | }
3416 | }
3417 | );
3418 |
3419 | /**
3420 | * Delete entities, relations, and observations.
3421 | */
3422 | server.tool(
3423 | "deletecontext",
3424 | toolDescriptions["deletecontext"],
3425 | {
3426 | type: z.enum(["entities", "relations", "observations"]).describe("Type of deletion operation: 'entities', 'relations', or 'observations'"),
3427 | data: z.array(z.any()).describe("Data for the deletion operation, structure varies by type but must be an array")
3428 | },
3429 | async ({ type, data }) => {
3430 | try {
3431 | switch (type) {
3432 | case "entities":
3433 | await knowledgeGraphManager.deleteEntities(data);
3434 | return {
3435 | content: [{
3436 | type: "text",
3437 | text: JSON.stringify({ success: true, message: `Deleted ${data.length} entities` }, null, 2)
3438 | }]
3439 | };
3440 |
3441 | case "relations":
3442 | // Ensure relations match the Relation interface
3443 | const typedRelations: Relation[] = data.map((r: any) => ({
3444 | from: r.from,
3445 | to: r.to,
3446 | relationType: r.relationType
3447 | }));
3448 | await knowledgeGraphManager.deleteRelations(typedRelations);
3449 | return {
3450 | content: [{
3451 | type: "text",
3452 | text: JSON.stringify({ success: true, message: `Deleted ${data.length} relations` }, null, 2)
3453 | }]
3454 | };
3455 |
3456 | case "observations":
3457 | // Ensure deletions match the required interface
3458 | const typedDeletions: { entityName: string; observations: string[] }[] = data.map((d: any) => ({
3459 | entityName: d.entityName,
3460 | observations: d.observations
3461 | }));
3462 | await knowledgeGraphManager.deleteObservations(typedDeletions);
3463 | return {
3464 | content: [{
3465 | type: "text",
3466 | text: JSON.stringify({ success: true, message: `Deleted observations from ${data.length} entities` }, null, 2)
3467 | }]
3468 | };
3469 |
3470 | default:
3471 | throw new Error(`Invalid type: ${type}. Must be 'entities', 'relations', or 'observations'.`);
3472 | }
3473 | } catch (error) {
3474 | return {
3475 | content: [{
3476 | type: "text",
3477 | text: JSON.stringify({
3478 | success: false,
3479 | error: error instanceof Error ? error.message : String(error)
3480 | }, null, 2)
3481 | }]
3482 | };
3483 | }
3484 | }
3485 | );
3486 |
3487 | /**
3488 | * Read the graph, search nodes, open nodes, get project overview, get task dependencies, get team member assignments, get milestone progress, get project timeline, get resource allocation, get project risks, find related projects, get decision log, and get project health.
3489 | */
3490 | server.tool(
3491 | "advancedcontext",
3492 | toolDescriptions["advancedcontext"],
3493 | {
3494 | type: z.enum([
3495 | "graph",
3496 | "search",
3497 | "nodes",
3498 | "project",
3499 | "dependencies",
3500 | "assignments",
3501 | "milestones",
3502 | "timeline",
3503 | "resources",
3504 | "risks",
3505 | "related",
3506 | "decisions",
3507 | "health"
3508 | ]).describe("Type of get operation"),
3509 | params: z.record(z.string(), z.any()).describe("Parameters for the get operation, structure varies by type")
3510 | },
3511 | async ({ type, params }) => {
3512 | try {
3513 | let result;
3514 |
3515 | switch (type) {
3516 | case "graph":
3517 | result = await knowledgeGraphManager.readGraph();
3518 | return {
3519 | content: [{
3520 | type: "text",
3521 | text: JSON.stringify({ success: true, graph: result }, null, 2)
3522 | }]
3523 | };
3524 |
3525 | case "search":
3526 | result = await knowledgeGraphManager.searchNodes(params.query);
3527 | return {
3528 | content: [{
3529 | type: "text",
3530 | text: JSON.stringify({ success: true, results: result }, null, 2)
3531 | }]
3532 | };
3533 |
3534 | case "nodes":
3535 | result = await knowledgeGraphManager.openNodes(params.names);
3536 | return {
3537 | content: [{
3538 | type: "text",
3539 | text: JSON.stringify({ success: true, nodes: result }, null, 2)
3540 | }]
3541 | };
3542 |
3543 | case "project":
3544 | result = await knowledgeGraphManager.getProjectOverview(params.projectName);
3545 | return {
3546 | content: [{
3547 | type: "text",
3548 | text: JSON.stringify({ success: true, project: result }, null, 2)
3549 | }]
3550 | };
3551 |
3552 | case "dependencies":
3553 | result = await knowledgeGraphManager.getTaskDependencies(
3554 | params.taskName,
3555 | params.depth || 2
3556 | );
3557 | return {
3558 | content: [{
3559 | type: "text",
3560 | text: JSON.stringify({ success: true, dependencies: result }, null, 2)
3561 | }]
3562 | };
3563 |
3564 | case "assignments":
3565 | result = await knowledgeGraphManager.getTeamMemberAssignments(params.teamMemberName);
3566 | return {
3567 | content: [{
3568 | type: "text",
3569 | text: JSON.stringify({ success: true, assignments: result }, null, 2)
3570 | }]
3571 | };
3572 |
3573 | case "milestones":
3574 | result = await knowledgeGraphManager.getMilestoneProgress(
3575 | params.projectName,
3576 | params.milestoneName
3577 | );
3578 | return {
3579 | content: [{
3580 | type: "text",
3581 | text: JSON.stringify({ success: true, milestones: result }, null, 2)
3582 | }]
3583 | };
3584 |
3585 | case "timeline":
3586 | result = await knowledgeGraphManager.getProjectTimeline(params.projectName);
3587 | return {
3588 | content: [{
3589 | type: "text",
3590 | text: JSON.stringify({ success: true, timeline: result }, null, 2)
3591 | }]
3592 | };
3593 |
3594 | case "resources":
3595 | result = await knowledgeGraphManager.getResourceAllocation(
3596 | params.projectName,
3597 | params.resourceName
3598 | );
3599 | return {
3600 | content: [{
3601 | type: "text",
3602 | text: JSON.stringify({ success: true, resources: result }, null, 2)
3603 | }]
3604 | };
3605 |
3606 | case "risks":
3607 | result = await knowledgeGraphManager.getProjectRisks(params.projectName);
3608 | return {
3609 | content: [{
3610 | type: "text",
3611 | text: JSON.stringify({ success: true, risks: result }, null, 2)
3612 | }]
3613 | };
3614 |
3615 | case "related":
3616 | result = await knowledgeGraphManager.findRelatedProjects(
3617 | params.projectName,
3618 | params.depth || 1
3619 | );
3620 | return {
3621 | content: [{
3622 | type: "text",
3623 | text: JSON.stringify({ success: true, relatedProjects: result }, null, 2)
3624 | }]
3625 | };
3626 |
3627 | case "decisions":
3628 | result = await knowledgeGraphManager.getDecisionLog(params.projectName);
3629 | return {
3630 | content: [{
3631 | type: "text",
3632 | text: JSON.stringify({ success: true, decisions: result }, null, 2)
3633 | }]
3634 | };
3635 |
3636 | case "health":
3637 | result = await knowledgeGraphManager.getProjectHealth(params.projectName);
3638 | return {
3639 | content: [{
3640 | type: "text",
3641 | text: JSON.stringify({ success: true, health: result }, null, 2)
3642 | }]
3643 | };
3644 |
3645 | default:
3646 | throw new Error(`Invalid type: ${type}. Must be one of the supported get operation types.`);
3647 | }
3648 | } catch (error) {
3649 | return {
3650 | content: [{
3651 | type: "text",
3652 | text: JSON.stringify({
3653 | success: false,
3654 | error: error instanceof Error ? error.message : String(error)
3655 | }, null, 2)
3656 | }]
3657 | };
3658 | }
3659 | }
3660 | );
3661 |
3662 | // Connect the server to the transport
3663 | const transport = new StdioServerTransport();
3664 | await server.connect(transport);
3665 |
3666 | } catch (error) {
3667 | console.error("Error starting server:", error);
3668 | process.exit(1);
3669 | }
3670 | }
3671 |
3672 | // Start the server
3673 | main().catch(error => {
3674 | console.error('Fatal error:', error);
3675 | process.exit(1);
3676 | });
3677 |
3678 | // Export the KnowledgeGraphManager class for testing
3679 | export { KnowledgeGraphManager };
```