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