bobs-workshop 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +252 -0
  3. package/bin/bobs-mcp.js +130 -0
  4. package/dist/api/taskLogger.js +106 -0
  5. package/dist/api/taskLogger.js.map +1 -0
  6. package/dist/cli/checker.js +401 -0
  7. package/dist/cli/checker.js.map +1 -0
  8. package/dist/cli/cleanup.js +131 -0
  9. package/dist/cli/cleanup.js.map +1 -0
  10. package/dist/cli/debug.js +157 -0
  11. package/dist/cli/debug.js.map +1 -0
  12. package/dist/cli/health.js +97 -0
  13. package/dist/cli/health.js.map +1 -0
  14. package/dist/cli/setup.js +81 -0
  15. package/dist/cli/setup.js.map +1 -0
  16. package/dist/cli/workshop.js +42 -0
  17. package/dist/cli/workshop.js.map +1 -0
  18. package/dist/dashboard/server.js +1206 -0
  19. package/dist/dashboard/server.js.map +1 -0
  20. package/dist/index.js +757 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/prompts/architect.js +157 -0
  23. package/dist/prompts/architect.js.map +1 -0
  24. package/dist/prompts/debugger.js +201 -0
  25. package/dist/prompts/debugger.js.map +1 -0
  26. package/dist/prompts/engineer.js +171 -0
  27. package/dist/prompts/engineer.js.map +1 -0
  28. package/dist/prompts/orchestrator.js +225 -0
  29. package/dist/prompts/orchestrator.js.map +1 -0
  30. package/dist/prompts/reviewer.js +199 -0
  31. package/dist/prompts/reviewer.js.map +1 -0
  32. package/dist/services/activitySummarizer.js +353 -0
  33. package/dist/services/activitySummarizer.js.map +1 -0
  34. package/dist/services/changeValidator.js +396 -0
  35. package/dist/services/changeValidator.js.map +1 -0
  36. package/dist/services/claudeOrchestrator.js +343 -0
  37. package/dist/services/claudeOrchestrator.js.map +1 -0
  38. package/dist/services/fileMonitor.js +250 -0
  39. package/dist/services/fileMonitor.js.map +1 -0
  40. package/dist/services/implementationSummarizer.js +306 -0
  41. package/dist/services/implementationSummarizer.js.map +1 -0
  42. package/dist/services/liveMonitor.js +315 -0
  43. package/dist/services/liveMonitor.js.map +1 -0
  44. package/dist/services/mcpAuditLogger.js +104 -0
  45. package/dist/services/mcpAuditLogger.js.map +1 -0
  46. package/dist/services/mcpLogger.js +223 -0
  47. package/dist/services/mcpLogger.js.map +1 -0
  48. package/dist/services/tmuxManager.js +541 -0
  49. package/dist/services/tmuxManager.js.map +1 -0
  50. package/dist/tools/approvalTools.js +244 -0
  51. package/dist/tools/approvalTools.js.map +1 -0
  52. package/dist/tools/autoDebugger.js +147 -0
  53. package/dist/tools/autoDebugger.js.map +1 -0
  54. package/dist/tools/cleanupService.js +221 -0
  55. package/dist/tools/cleanupService.js.map +1 -0
  56. package/dist/tools/dashboardTools.js +359 -0
  57. package/dist/tools/dashboardTools.js.map +1 -0
  58. package/dist/tools/developmentNudges.js +336 -0
  59. package/dist/tools/developmentNudges.js.map +1 -0
  60. package/dist/tools/gitTools.js +741 -0
  61. package/dist/tools/gitTools.js.map +1 -0
  62. package/dist/tools/orchestratorTools.js +765 -0
  63. package/dist/tools/orchestratorTools.js.map +1 -0
  64. package/dist/tools/searchTools.js +788 -0
  65. package/dist/tools/searchTools.js.map +1 -0
  66. package/dist/tools/specTools.js +350 -0
  67. package/dist/tools/specTools.js.map +1 -0
  68. package/dist/tools/tmuxTools.js +100 -0
  69. package/dist/tools/tmuxTools.js.map +1 -0
  70. package/dist/tools/workRecorder.js +215 -0
  71. package/dist/tools/workRecorder.js.map +1 -0
  72. package/dist/tools/worktreeTools.js +705 -0
  73. package/dist/tools/worktreeTools.js.map +1 -0
  74. package/dist/utils/__tests__/integration.test.js +57 -0
  75. package/dist/utils/__tests__/integration.test.js.map +1 -0
  76. package/dist/utils/__tests__/serverDetection.test.js +151 -0
  77. package/dist/utils/__tests__/serverDetection.test.js.map +1 -0
  78. package/dist/utils/errorHandling.js +336 -0
  79. package/dist/utils/errorHandling.js.map +1 -0
  80. package/dist/utils/processManager.js +172 -0
  81. package/dist/utils/processManager.js.map +1 -0
  82. package/dist/utils/reliability.js +263 -0
  83. package/dist/utils/reliability.js.map +1 -0
  84. package/dist/utils/responseFormatter.js +250 -0
  85. package/dist/utils/responseFormatter.js.map +1 -0
  86. package/dist/utils/serverDetection.js +133 -0
  87. package/dist/utils/serverDetection.js.map +1 -0
  88. package/dist/utils/specMigration.js +105 -0
  89. package/dist/utils/specMigration.js.map +1 -0
  90. package/dist/validation/schemas.js +299 -0
  91. package/dist/validation/schemas.js.map +1 -0
  92. package/package.json +79 -0
  93. package/scripts/init-workspace.js +63 -0
  94. package/scripts/install-search-tools.js +116 -0
@@ -0,0 +1,1206 @@
1
+ // src/dashboard/server.ts
2
+ import express from "express";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import fs from "fs-extra";
6
+ import { specCreateHandler, specUpdateHandler, specGetHandler, specListHandler } from "../tools/specTools.js";
7
+ import { worktreeCreateHandler, worktreeListHandler, worktreeMergeHandler, worktreeRemoveHandler } from "../tools/worktreeTools.js";
8
+ import { semgrepSearchHandler, enhancedFileSearchHandler } from "../tools/searchTools.js";
9
+ import { createApprovalRequest, processApprovalResponse, checkApprovalStatus, handleApprovalTimeout } from "../tools/approvalTools.js";
10
+ import { fileMonitor } from "../services/fileMonitor.js";
11
+ import { mcpLogger } from "../services/mcpLogger.js";
12
+ import multer from "multer";
13
+ import { taskService, createErrorResponse, createSuccessResponse } from "../api/taskLogger.js";
14
+ class NotificationManager {
15
+ constructor() {
16
+ this.notifications = [];
17
+ this.maxNotifications = 1000;
18
+ }
19
+ addNotification(notification) {
20
+ const fullNotification = {
21
+ id: `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
22
+ ...notification
23
+ };
24
+ this.notifications.push(fullNotification);
25
+ // Keep only the last N notifications
26
+ if (this.notifications.length > this.maxNotifications) {
27
+ this.notifications = this.notifications.slice(-this.maxNotifications);
28
+ }
29
+ console.log(`📢 Dashboard notification: [${fullNotification.spec_id || 'system'}] ${fullNotification.event}`);
30
+ return fullNotification;
31
+ }
32
+ getRecentNotifications(limit = 50) {
33
+ return this.notifications.slice(-limit);
34
+ }
35
+ getNotificationsBySpec(spec_id, limit = 20) {
36
+ return this.notifications
37
+ .filter(n => n.spec_id === spec_id)
38
+ .slice(-limit);
39
+ }
40
+ clearOldNotifications(olderThanHours = 24) {
41
+ const cutoff = new Date(Date.now() - (olderThanHours * 60 * 60 * 1000));
42
+ const originalLength = this.notifications.length;
43
+ this.notifications = this.notifications.filter(n => new Date(n.timestamp) > cutoff);
44
+ return originalLength - this.notifications.length;
45
+ }
46
+ }
47
+ const notificationManager = new NotificationManager();
48
+ // Helper functions for enhanced file change descriptions
49
+ function generateFileChangeDescription(change) {
50
+ const fileName = change.file.split('/').pop() || change.file;
51
+ const fileExt = change.file.split('.').pop() || '';
52
+ let action = '';
53
+ let actionDetails = '';
54
+ switch (change.type) {
55
+ case 'added':
56
+ action = '➕ Created';
57
+ actionDetails = 'New file added to project';
58
+ break;
59
+ case 'changed':
60
+ action = '✏️ Modified';
61
+ actionDetails = 'Existing file updated with changes';
62
+ break;
63
+ case 'unlinked':
64
+ action = '🗑️ Deleted';
65
+ actionDetails = 'File removed from project';
66
+ break;
67
+ default:
68
+ action = '📝 Updated';
69
+ actionDetails = 'File content or structure changed';
70
+ }
71
+ const fileTypeInfo = getFileTypeDescription(fileExt);
72
+ const timeAgo = getTimeAgo(change.timestamp);
73
+ const linesInfo = change.lines_changed ? ` (${change.lines_changed} lines)` : '';
74
+ return `${action} ${fileTypeInfo} ${fileName}${linesInfo} • ${actionDetails} ${timeAgo}`;
75
+ }
76
+ function categorizeFileChange(filePath) {
77
+ if (filePath.includes('test') || filePath.includes('spec'))
78
+ return 'test';
79
+ if (filePath.includes('util') || filePath.includes('helper'))
80
+ return 'utility';
81
+ if (filePath.includes('component') || filePath.includes('ui'))
82
+ return 'component';
83
+ if (filePath.includes('service') || filePath.includes('api'))
84
+ return 'service';
85
+ if (filePath.includes('config') || filePath.endsWith('.json'))
86
+ return 'config';
87
+ if (filePath.includes('style') || filePath.endsWith('.css'))
88
+ return 'style';
89
+ if (filePath.endsWith('.md') || filePath.includes('doc'))
90
+ return 'documentation';
91
+ return 'core';
92
+ }
93
+ function getFileTypeDescription(ext) {
94
+ const typeMap = {
95
+ 'ts': 'TypeScript file',
96
+ 'js': 'JavaScript file',
97
+ 'json': 'JSON config',
98
+ 'md': 'documentation',
99
+ 'css': 'stylesheet',
100
+ 'html': 'HTML file',
101
+ 'vue': 'Vue component',
102
+ 'jsx': 'React component',
103
+ 'tsx': 'React TypeScript component',
104
+ 'yml': 'YAML config',
105
+ 'yaml': 'YAML config'
106
+ };
107
+ return typeMap[ext] || 'file';
108
+ }
109
+ async function getFileGitDiff(filePath) {
110
+ try {
111
+ const { simpleGit } = await import('simple-git');
112
+ const gitInstance = simpleGit(process.cwd());
113
+ // Get the diff for the file (current worktree vs main branch to show all changes)
114
+ const diff = await gitInstance.diff(['--numstat', 'main', '--', filePath]);
115
+ if (!diff.trim()) {
116
+ return null;
117
+ }
118
+ const lines = diff.trim().split('\n');
119
+ const stats = lines[0].split('\t');
120
+ if (stats.length >= 2) {
121
+ const linesAdded = parseInt(stats[0]) || 0;
122
+ const linesRemoved = parseInt(stats[1]) || 0;
123
+ let diffSummary = '';
124
+ if (linesAdded > 0 && linesRemoved > 0) {
125
+ diffSummary = `+${linesAdded} -${linesRemoved}`;
126
+ }
127
+ else if (linesAdded > 0) {
128
+ diffSummary = `+${linesAdded}`;
129
+ }
130
+ else if (linesRemoved > 0) {
131
+ diffSummary = `-${linesRemoved}`;
132
+ }
133
+ return { linesAdded, linesRemoved, diffSummary };
134
+ }
135
+ return null;
136
+ }
137
+ catch (error) {
138
+ console.error('Error getting git diff for file:', filePath, error);
139
+ return null;
140
+ }
141
+ }
142
+ function generateHumanReadableActivityDescription(activity) {
143
+ if (!activity)
144
+ return 'Unknown activity';
145
+ // Handle verbose mode-specific details first
146
+ if (activity.data?.verbose_details) {
147
+ const verboseDescription = generateVerboseActivityDescription(activity);
148
+ if (verboseDescription)
149
+ return verboseDescription;
150
+ }
151
+ // Handle different types of activities
152
+ if (activity.type === 'mcp_operation') {
153
+ return generateMcpOperationDescription(activity);
154
+ }
155
+ if (activity.action) {
156
+ return generateActionDescription(activity);
157
+ }
158
+ if (activity.note) {
159
+ return activity.note;
160
+ }
161
+ return 'Activity completed';
162
+ }
163
+ function generateVerboseActivityDescription(activity) {
164
+ const mode = activity.data?.mode;
165
+ const verbose = activity.data?.verbose_details;
166
+ if (!mode || !verbose)
167
+ return null;
168
+ switch (mode) {
169
+ case 'engineer':
170
+ if (verbose.engineer) {
171
+ const eng = verbose.engineer;
172
+ let description = '';
173
+ if (eng.task_description) {
174
+ description += `🔧 ${eng.task_description}`;
175
+ }
176
+ if (eng.completion_percentage !== undefined) {
177
+ description += ` (${eng.completion_percentage}% complete)`;
178
+ }
179
+ if (eng.tasks_completed?.length) {
180
+ description += ` • Completed: ${eng.tasks_completed.join(', ')}`;
181
+ }
182
+ if (eng.files_modified?.length) {
183
+ description += ` • Files: ${eng.files_modified.length} modified`;
184
+ }
185
+ if (eng.build_status === 'success') {
186
+ description += ' ✅ Build passed';
187
+ }
188
+ else if (eng.build_status === 'failed') {
189
+ description += ' ❌ Build failed';
190
+ }
191
+ if (eng.test_status === 'passed') {
192
+ description += ' 🧪 Tests passed';
193
+ }
194
+ else if (eng.test_status === 'failed') {
195
+ description += ' 🧪 Tests failed';
196
+ }
197
+ return description || 'Engineer task completed';
198
+ }
199
+ break;
200
+ case 'debugger':
201
+ if (verbose.debugger) {
202
+ const dbg = verbose.debugger;
203
+ let description = '';
204
+ if (dbg.issue_description) {
205
+ description += `🐛 Issue: ${dbg.issue_description}`;
206
+ }
207
+ if (dbg.root_cause) {
208
+ description += ` • Root cause: ${dbg.root_cause}`;
209
+ }
210
+ if (dbg.fix_description) {
211
+ description += ` • Fix: ${dbg.fix_description}`;
212
+ }
213
+ if (dbg.confidence_level) {
214
+ const confidenceIcon = dbg.confidence_level === 'high' ? '🎯' :
215
+ dbg.confidence_level === 'medium' ? '⚖️' : '❓';
216
+ description += ` ${confidenceIcon} Confidence: ${dbg.confidence_level}`;
217
+ }
218
+ if (dbg.files_affected?.length) {
219
+ description += ` • Affected: ${dbg.files_affected.length} files`;
220
+ }
221
+ return description || 'Debug task completed';
222
+ }
223
+ break;
224
+ case 'architect':
225
+ if (verbose.architect) {
226
+ const arch = verbose.architect;
227
+ let description = '';
228
+ if (arch.spec_sections_completed?.length) {
229
+ description += `📋 Completed: ${arch.spec_sections_completed.join(', ')}`;
230
+ }
231
+ if (arch.research_findings?.length) {
232
+ description += ` • Research: ${arch.research_findings.length} findings`;
233
+ }
234
+ if (arch.design_decisions?.length) {
235
+ description += ` • Decisions: ${arch.design_decisions.length} made`;
236
+ }
237
+ if (arch.codebase_insights?.length) {
238
+ description += ` • Insights: ${arch.codebase_insights.length} discovered`;
239
+ }
240
+ if (arch.architecture_patterns?.length) {
241
+ description += ` • Patterns: ${arch.architecture_patterns.join(', ')}`;
242
+ }
243
+ return description || 'Architecture task completed';
244
+ }
245
+ break;
246
+ }
247
+ return null;
248
+ }
249
+ function generateMcpOperationDescription(activity) {
250
+ const tool = activity.tool_name || activity.operation || 'unknown';
251
+ const status = activity.status || 'completed';
252
+ const toolDescriptions = {
253
+ 'bob.workshop': '🔧 Workshop orchestration',
254
+ 'bob.manual.create': '📝 Manual creation',
255
+ 'bob.manual.update': '✏️ Manual update',
256
+ 'bob.manual.get': '📖 Manual retrieval',
257
+ 'bob.workflow.start': '🚀 Workflow initiation',
258
+ 'bob.workflow.deploy': '🚢 Deployment process',
259
+ 'bob.code.search': '🔍 Code search',
260
+ 'bob.validate.changes': '✅ Change validation',
261
+ 'bob.summarize.implementation': '📊 Implementation summary',
262
+ 'bob.dashboard.launch': '💻 Dashboard launch'
263
+ };
264
+ const description = toolDescriptions[tool] || `🔧 ${tool.replace('bob.', '').replace(/[._]/g, ' ')}`;
265
+ const statusIcon = status === 'success' ? '✅' : status === 'error' ? '❌' : '⏳';
266
+ return `${description} ${statusIcon}`;
267
+ }
268
+ function generateActionDescription(activity) {
269
+ const action = activity.action || '';
270
+ const actionDescriptions = {
271
+ 'implementation_complete': '✅ Code implementation completed successfully',
272
+ 'validation_compliant': '✅ Validation passed - changes are compliant',
273
+ 'validation_partial': '⚠️ Validation completed with some issues',
274
+ 'validation_non_compliant': '❌ Validation failed - changes need review',
275
+ 'worktree_created': '🌳 Development workspace created',
276
+ 'worktree_merged': '🔄 Changes merged to main branch',
277
+ 'worktree_removed': '🗑️ Development workspace removed',
278
+ 'worktree_status_changed': '🔄 Workspace status updated',
279
+ 'commit_created': '💾 Changes committed to repository',
280
+ 'files_modified': '📝 Files updated in project',
281
+ 'test_run': '🧪 Tests executed',
282
+ 'build_completed': '🏗️ Build process completed',
283
+ 'deployment_started': '🚀 Deployment initiated',
284
+ 'deployment_completed': '✅ Deployment completed successfully',
285
+ 'debug_session': '🐛 Debug session completed',
286
+ 'code_review': '👀 Code review performed',
287
+ 'documentation_updated': '📚 Documentation updated'
288
+ };
289
+ // Try exact match first
290
+ if (actionDescriptions[action]) {
291
+ return actionDescriptions[action];
292
+ }
293
+ // Try partial matches
294
+ if (action.includes('implement'))
295
+ return '🛠️ Implementation work completed';
296
+ if (action.includes('validate') || action.includes('validation'))
297
+ return '✅ Validation process completed';
298
+ if (action.includes('commit'))
299
+ return '💾 Code changes committed';
300
+ if (action.includes('merge'))
301
+ return '🔄 Code changes merged';
302
+ if (action.includes('test'))
303
+ return '🧪 Testing completed';
304
+ if (action.includes('build'))
305
+ return '🏗️ Build process completed';
306
+ if (action.includes('deploy'))
307
+ return '🚀 Deployment completed';
308
+ if (action.includes('debug'))
309
+ return '🐛 Debug process completed';
310
+ if (action.includes('review'))
311
+ return '👀 Review completed';
312
+ if (action.includes('create'))
313
+ return '✨ Creation completed';
314
+ if (action.includes('update') || action.includes('modify'))
315
+ return '✏️ Update completed';
316
+ if (action.includes('delete') || action.includes('remove'))
317
+ return '🗑️ Removal completed';
318
+ // Fallback: humanize the action name
319
+ const humanized = action
320
+ .replace(/[_-]/g, ' ')
321
+ .replace(/\b\w/g, (l) => l.toUpperCase());
322
+ return `📋 ${humanized}`;
323
+ }
324
+ function getTimeAgo(timestamp) {
325
+ const now = new Date();
326
+ const changeTime = new Date(timestamp);
327
+ const diffMs = now.getTime() - changeTime.getTime();
328
+ const diffMins = Math.floor(diffMs / (1000 * 60));
329
+ if (diffMins < 1)
330
+ return 'just now';
331
+ if (diffMins < 60)
332
+ return `${diffMins}m ago`;
333
+ const diffHours = Math.floor(diffMins / 60);
334
+ if (diffHours < 24)
335
+ return `${diffHours}h ago`;
336
+ const diffDays = Math.floor(diffHours / 24);
337
+ return `${diffDays}d ago`;
338
+ }
339
+ // Compatibility for import.meta in different environments
340
+ const getFilename = () => {
341
+ // Check if we're in a test environment
342
+ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
343
+ return "/src/dashboard/server.ts";
344
+ }
345
+ // Check if import.meta is available
346
+ if (typeof import.meta !== 'undefined' && import.meta.url) {
347
+ return fileURLToPath(import.meta.url);
348
+ }
349
+ // Fallback
350
+ return "/src/dashboard/server.ts";
351
+ };
352
+ const __filename = getFilename();
353
+ const __dirname = path.dirname(__filename);
354
+ const projectRoot = path.resolve(__dirname, "../..");
355
+ export function createDashboardServer() {
356
+ const app = express();
357
+ app.use(express.json());
358
+ app.use(express.static(path.join(projectRoot, "public")));
359
+ app.use("/src", express.static(path.join(projectRoot, "src")));
360
+ // Multer configuration for image uploads
361
+ const storage = multer.memoryStorage();
362
+ const upload = multer({
363
+ storage,
364
+ limits: {
365
+ fileSize: 5 * 1024 * 1024, // 5MB limit
366
+ },
367
+ fileFilter: (req, file, cb) => {
368
+ // Accept common image formats
369
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'];
370
+ if (allowedTypes.includes(file.mimetype)) {
371
+ cb(null, true);
372
+ }
373
+ else {
374
+ cb(new Error('Invalid file type. Only PNG, JPEG, GIF, and WebP are allowed.'));
375
+ }
376
+ }
377
+ });
378
+ // SPEC management endpoints
379
+ app.post("/api/tools/bob.spec.create", async (req, res) => {
380
+ try {
381
+ const result = await specCreateHandler(req.body);
382
+ res.json(result);
383
+ }
384
+ catch (error) {
385
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
386
+ }
387
+ });
388
+ app.post("/api/tools/bob.spec.update", async (req, res) => {
389
+ try {
390
+ const result = await specUpdateHandler(req.body);
391
+ res.json(result);
392
+ }
393
+ catch (error) {
394
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
395
+ }
396
+ });
397
+ app.post("/api/tools/bob.spec.get", async (req, res) => {
398
+ try {
399
+ const result = await specGetHandler(req.body);
400
+ res.json(result);
401
+ }
402
+ catch (error) {
403
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
404
+ }
405
+ });
406
+ app.get("/api/tools/bob.spec.list", async (req, res) => {
407
+ try {
408
+ const result = await specListHandler({ filter: req.query.filter });
409
+ res.json(result);
410
+ }
411
+ catch (error) {
412
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
413
+ }
414
+ });
415
+ app.post("/api/tools/bob.spec.delete", async (req, res) => {
416
+ try {
417
+ const { spec_id } = req.body;
418
+ if (!spec_id) {
419
+ return res.status(400).json({ error: "spec_id is required" });
420
+ }
421
+ const specsDir = path.resolve(process.cwd(), ".bob/specs");
422
+ const filePath = path.join(specsDir, `${spec_id}.json`);
423
+ const indexPath = path.join(specsDir, "index.json");
424
+ // Check if spec exists
425
+ if (!await fs.pathExists(filePath)) {
426
+ return res.status(404).json({ error: `SPEC ${spec_id} not found` });
427
+ }
428
+ // Delete the spec file
429
+ await fs.remove(filePath);
430
+ // Update index.json
431
+ if (await fs.pathExists(indexPath)) {
432
+ const indexData = await fs.readJson(indexPath);
433
+ indexData.specs = (indexData.specs || []).filter((spec) => spec.spec_id !== spec_id);
434
+ await fs.writeFile(indexPath, JSON.stringify(indexData, null, 2), "utf8");
435
+ }
436
+ res.json({ status: "deleted", spec_id });
437
+ }
438
+ catch (error) {
439
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
440
+ }
441
+ });
442
+ // Image upload endpoint for inline image support
443
+ app.post("/api/images/upload-inline", upload.single('image'), async (req, res) => {
444
+ try {
445
+ if (!req.file) {
446
+ return res.status(400).json({ error: "No image file provided" });
447
+ }
448
+ const workingDir = process.cwd();
449
+ const uploadsDir = path.join(workingDir, '.mcp', 'uploads');
450
+ // Ensure uploads directory exists
451
+ await fs.ensureDir(uploadsDir);
452
+ // Generate auto-increment filename
453
+ const files = await fs.readdir(uploadsDir);
454
+ const imageFiles = files.filter(f => /\.(png|jpg|jpeg|gif|webp)$/i.test(f));
455
+ const nextNumber = imageFiles.length + 1;
456
+ // Get file extension
457
+ const ext = path.extname(req.file.originalname) || '.png';
458
+ const filename = `img${nextNumber}${ext}`;
459
+ const filePath = path.join(uploadsDir, filename);
460
+ // Save file to uploads directory
461
+ await fs.writeFile(filePath, req.file.buffer);
462
+ // Return placeholder data for frontend
463
+ res.json({
464
+ success: true,
465
+ placeholder: {
466
+ id: `img${nextNumber}`,
467
+ filename: filename,
468
+ path: `./mcp/uploads/${filename}`,
469
+ size: req.file.size,
470
+ mimetype: req.file.mimetype
471
+ }
472
+ });
473
+ // Add notification
474
+ notificationManager.addNotification({
475
+ event: "image_uploaded",
476
+ data: {
477
+ filename: filename,
478
+ size: req.file.size,
479
+ path: `./mcp/uploads/${filename}`
480
+ },
481
+ timestamp: new Date().toISOString()
482
+ });
483
+ }
484
+ catch (error) {
485
+ console.error('Image upload error:', error);
486
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
487
+ }
488
+ });
489
+ // Worktree endpoints
490
+ app.get("/api/tools/bob.worktree.list", async (req, res) => {
491
+ try {
492
+ const result = await worktreeListHandler();
493
+ res.json(result);
494
+ }
495
+ catch (error) {
496
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
497
+ }
498
+ });
499
+ app.post("/api/tools/bob.worktree.create", async (req, res) => {
500
+ try {
501
+ const result = await worktreeCreateHandler(req.body);
502
+ res.json(result);
503
+ }
504
+ catch (error) {
505
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
506
+ }
507
+ });
508
+ app.post("/api/tools/bob.worktree.merge", async (req, res) => {
509
+ try {
510
+ const result = await worktreeMergeHandler(req.body);
511
+ res.json(result);
512
+ }
513
+ catch (error) {
514
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
515
+ }
516
+ });
517
+ app.post("/api/tools/bob.worktree.remove", async (req, res) => {
518
+ try {
519
+ const result = await worktreeRemoveHandler(req.body);
520
+ res.json(result);
521
+ }
522
+ catch (error) {
523
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
524
+ }
525
+ });
526
+ // Research endpoints
527
+ app.post("/api/tools/bob.search.enhanced", async (req, res) => {
528
+ try {
529
+ const result = await enhancedFileSearchHandler(req.body);
530
+ res.json(result);
531
+ }
532
+ catch (error) {
533
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
534
+ }
535
+ });
536
+ app.post("/api/tools/bob.search.semgrep", async (req, res) => {
537
+ try {
538
+ const result = await semgrepSearchHandler(req.body);
539
+ res.json(result);
540
+ }
541
+ catch (error) {
542
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
543
+ }
544
+ });
545
+ // User approval endpoints
546
+ app.get("/api/approvals/:spec_id", async (req, res) => {
547
+ try {
548
+ const { spec_id } = req.params;
549
+ const approval_type = req.query.approval_type;
550
+ const result = await checkApprovalStatus(spec_id, approval_type);
551
+ res.json(result);
552
+ }
553
+ catch (error) {
554
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
555
+ }
556
+ });
557
+ app.post("/api/approvals/request", async (req, res) => {
558
+ try {
559
+ const { spec_id, approval_type, message, context } = req.body;
560
+ if (!spec_id || !approval_type || !message) {
561
+ return res.status(400).json({
562
+ error: "spec_id, approval_type, and message are required"
563
+ });
564
+ }
565
+ const result = await createApprovalRequest({ spec_id, approval_type, message, context });
566
+ // Send real-time notification to dashboard
567
+ notificationManager.addNotification({
568
+ spec_id,
569
+ event: "approval_requested",
570
+ data: {
571
+ approval_type,
572
+ message,
573
+ approval_id: result.approval_id,
574
+ dashboard_url: result.dashboard_url
575
+ },
576
+ timestamp: new Date().toISOString()
577
+ });
578
+ res.json(result);
579
+ }
580
+ catch (error) {
581
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
582
+ }
583
+ });
584
+ app.post("/api/approvals/respond", async (req, res) => {
585
+ try {
586
+ const { spec_id, approval_type, approved, feedback, timestamp } = req.body;
587
+ if (!spec_id || !approval_type || typeof approved !== 'boolean') {
588
+ return res.status(400).json({
589
+ error: "spec_id, approval_type, and approved (boolean) are required"
590
+ });
591
+ }
592
+ const result = await processApprovalResponse({
593
+ spec_id,
594
+ approval_type,
595
+ approved,
596
+ feedback,
597
+ timestamp
598
+ });
599
+ // Send real-time notification to dashboard
600
+ notificationManager.addNotification({
601
+ spec_id,
602
+ event: "approval_responded",
603
+ data: {
604
+ approval_type,
605
+ approved,
606
+ feedback,
607
+ new_state: result.new_state,
608
+ approval_id: result.approval_id
609
+ },
610
+ timestamp: result.timestamp
611
+ });
612
+ res.json(result);
613
+ }
614
+ catch (error) {
615
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
616
+ }
617
+ });
618
+ app.post("/api/approvals/timeout", async (req, res) => {
619
+ try {
620
+ const { spec_id, approval_type } = req.body;
621
+ if (!spec_id || !approval_type) {
622
+ return res.status(400).json({
623
+ error: "spec_id and approval_type are required"
624
+ });
625
+ }
626
+ const result = await handleApprovalTimeout(spec_id, approval_type);
627
+ // Send real-time notification to dashboard
628
+ notificationManager.addNotification({
629
+ spec_id,
630
+ event: "approval_timeout",
631
+ data: {
632
+ approval_type,
633
+ approval_id: result.approval_id,
634
+ timeout_at: result.timeout_at
635
+ },
636
+ timestamp: new Date().toISOString()
637
+ });
638
+ res.json(result);
639
+ }
640
+ catch (error) {
641
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
642
+ }
643
+ });
644
+ // File monitoring endpoints
645
+ app.get("/api/file-changes", (req, res) => {
646
+ try {
647
+ const limit = parseInt(req.query.limit) || 50;
648
+ const changes = fileMonitor.getRecentChanges(limit);
649
+ res.json({ changes });
650
+ }
651
+ catch (error) {
652
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
653
+ }
654
+ });
655
+ app.get("/api/untracked-changes", (req, res) => {
656
+ try {
657
+ const untracked = fileMonitor.getUntrackedChanges();
658
+ res.json({ untracked });
659
+ }
660
+ catch (error) {
661
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
662
+ }
663
+ });
664
+ app.post("/api/assign-to-spec", async (req, res) => {
665
+ try {
666
+ const { files, spec_id } = req.body;
667
+ await fileMonitor.manuallyAssignToSpec(files, spec_id);
668
+ res.json({ status: "assigned" });
669
+ }
670
+ catch (error) {
671
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
672
+ }
673
+ });
674
+ // Enhanced dashboard data endpoint
675
+ app.get("/api/dashboard-data", async (req, res) => {
676
+ try {
677
+ const specsDir = path.resolve(projectRoot, ".bob/specs");
678
+ const indexPath = path.join(specsDir, "index.json");
679
+ if (!await fs.pathExists(indexPath)) {
680
+ return res.json({ specs: [], untracked: [], recent_changes: [] });
681
+ }
682
+ const indexData = await fs.readJson(indexPath);
683
+ const enhancedSpecs = [];
684
+ for (const specSummary of indexData.specs || []) {
685
+ const specPath = path.join(specsDir, `${specSummary.spec_id}.json`);
686
+ if (await fs.pathExists(specPath)) {
687
+ const specData = await fs.readJson(specPath);
688
+ // Parse implementation plan into task checklist
689
+ const implementationPlan = specData.implementation_plan || [];
690
+ const executionLogs = specData.execution_logs || specData.execution_log || [];
691
+ const taskChecklist = implementationPlan.map((task) => {
692
+ const isCompleted = executionLogs.some((log) => log.task_id === task.task_id);
693
+ const associatedLogs = executionLogs.filter((log) => log.task_id === task.task_id);
694
+ return {
695
+ task_id: task.task_id,
696
+ description: task.description,
697
+ completed: isCompleted,
698
+ execution_count: associatedLogs.length,
699
+ last_updated: associatedLogs.length > 0 ? associatedLogs[associatedLogs.length - 1].timestamp : null
700
+ };
701
+ });
702
+ // Create per-SPEC timeline: Prioritize enhanced_activities, fallback to legacy logs
703
+ const mcpOperations = specData.mcp_tool_operations || [];
704
+ // PRIORITY 1: Enhanced activities (new unified format)
705
+ const enhancedActivitiesTimeline = (specData.enhanced_activities || []).map(async (activity) => {
706
+ // Enrich files with real git diff data
707
+ const enrichedFiles = await Promise.all((activity.files_changed || []).map(async (file) => {
708
+ const gitDiff = await getFileGitDiff(file.path);
709
+ return {
710
+ ...file,
711
+ lines_added: gitDiff?.linesAdded || file.lines_added || 0,
712
+ lines_removed: gitDiff?.linesRemoved || file.lines_removed || 0,
713
+ diff_summary: gitDiff?.diffSummary || file.diff_summary || ''
714
+ };
715
+ }));
716
+ return {
717
+ ...activity,
718
+ files_changed: enrichedFiles,
719
+ human_description: activity.summary,
720
+ is_enhanced: true // Flag to identify enhanced activities in frontend
721
+ };
722
+ });
723
+ const resolvedEnhancedActivities = await Promise.all(enhancedActivitiesTimeline);
724
+ // PRIORITY 2: Legacy logs (for backward compatibility)
725
+ const legacyTimeline = specData.enhanced_activities && specData.enhanced_activities.length > 0 ? [] : [
726
+ ...executionLogs.map((log) => ({
727
+ ...log,
728
+ type: 'execution',
729
+ role: 'engineer',
730
+ human_description: generateHumanReadableActivityDescription(log),
731
+ is_enhanced: false
732
+ })),
733
+ ...(specData.debug_logs || specData.debug_log || []).map((log) => ({
734
+ ...log,
735
+ type: 'debug',
736
+ role: 'debugger',
737
+ human_description: generateHumanReadableActivityDescription(log),
738
+ is_enhanced: false
739
+ })),
740
+ ...(specData.activity || []).map((activity) => ({
741
+ ...activity,
742
+ type: activity.type || 'activity',
743
+ role: activity.role || 'system',
744
+ action: activity.action || activity.note || 'Activity logged',
745
+ human_description: generateHumanReadableActivityDescription(activity),
746
+ is_enhanced: false
747
+ }))
748
+ ];
749
+ // Add MCP operations to timeline
750
+ const mcpTimeline = mcpOperations.map((op) => ({
751
+ timestamp: op.timestamp,
752
+ type: 'mcp_operation',
753
+ role: 'mcp_system',
754
+ tool_name: op.tool_name,
755
+ success: op.success,
756
+ duration_ms: op.duration_ms,
757
+ parameters: op.input_parameters,
758
+ error_message: op.error_message,
759
+ human_description: generateHumanReadableActivityDescription(op),
760
+ is_enhanced: false
761
+ }));
762
+ // Combine and sort by timestamp
763
+ const timeline = [
764
+ ...resolvedEnhancedActivities,
765
+ ...legacyTimeline,
766
+ ...mcpTimeline
767
+ ].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
768
+ // Get file changes for this SPEC
769
+ const fileChanges = specData.file_changes || [];
770
+ // Get approval information for this SPEC
771
+ let approvalInfo = {
772
+ pending_approvals: [],
773
+ recent_approvals: [],
774
+ approval_history: []
775
+ };
776
+ try {
777
+ const approvalStatus = await checkApprovalStatus(specSummary.spec_id);
778
+ approvalInfo = {
779
+ pending_approvals: approvalStatus.pending_approvals || [],
780
+ recent_approvals: approvalStatus.all_approvals?.filter((a) => a.status === 'approved' || a.status === 'rejected').slice(-3) || [], // Last 3 completed approvals
781
+ approval_history: approvalStatus.all_approvals || []
782
+ };
783
+ }
784
+ catch (error) {
785
+ console.log(`Failed to get approval status for ${specSummary.spec_id}:`, error);
786
+ }
787
+ // Determine if awaiting user input
788
+ const awaitingApproval = approvalInfo.pending_approvals.length > 0;
789
+ const currentApprovalType = awaitingApproval ? approvalInfo.pending_approvals[0]?.approval_type : null;
790
+ // Get change validation information
791
+ const changeValidations = specData.change_validations || [];
792
+ const latestValidation = changeValidations.length > 0 ? changeValidations[changeValidations.length - 1] : null;
793
+ // Get implementation summaries
794
+ const implementationSummaries = specData.implementation_summaries || [];
795
+ const latestSummary = implementationSummaries.length > 0 ? implementationSummaries[implementationSummaries.length - 1] : null;
796
+ // Process file changes into human-readable format with validation status
797
+ const enhancedFileChanges = await Promise.all(fileChanges.slice(-20).map(async (change) => {
798
+ // Find validation data for this file
799
+ const validationData = latestValidation?.file_analyses?.find((analysis) => analysis.file === change.file);
800
+ // Get git diff information
801
+ const gitDiff = await getFileGitDiff(change.file);
802
+ return {
803
+ ...change,
804
+ humanReadable: generateFileChangeDescription(change),
805
+ category: categorizeFileChange(change.file),
806
+ compliance_status: validationData?.compliance || 'unknown',
807
+ compliance_reason: validationData?.reason,
808
+ lines_changed: validationData?.lineCount || (gitDiff ? gitDiff.linesAdded + gitDiff.linesRemoved : 0),
809
+ human_description: validationData?.humanSummary || generateFileChangeDescription(change),
810
+ git_diff: gitDiff ? {
811
+ lines_added: gitDiff.linesAdded,
812
+ lines_removed: gitDiff.linesRemoved,
813
+ diff_summary: gitDiff.diffSummary,
814
+ total_changes: gitDiff.linesAdded + gitDiff.linesRemoved
815
+ } : null
816
+ };
817
+ }));
818
+ // Extract worktree metadata from SPEC
819
+ const worktreeInfo = specData.worktree || {
820
+ branch: '',
821
+ path: '',
822
+ status: 'pending',
823
+ created_at: '',
824
+ removed_at: ''
825
+ };
826
+ enhancedSpecs.push({
827
+ ...specSummary,
828
+ task_checklist: taskChecklist,
829
+ timeline,
830
+ file_changes: enhancedFileChanges,
831
+ completion_percentage: Math.round((taskChecklist.filter((t) => t.completed).length / Math.max(taskChecklist.length, 1)) * 100),
832
+ approval_info: approvalInfo,
833
+ awaiting_approval: awaitingApproval,
834
+ current_approval_type: currentApprovalType,
835
+ user_action_required: awaitingApproval,
836
+ worktree: {
837
+ branch: worktreeInfo.branch,
838
+ path: worktreeInfo.path,
839
+ status: worktreeInfo.status,
840
+ created_at: worktreeInfo.created_at,
841
+ removed_at: worktreeInfo.removed_at,
842
+ is_active: worktreeInfo.status === 'active',
843
+ display_path: worktreeInfo.path ? worktreeInfo.path.replace(process.cwd(), '.') : ''
844
+ },
845
+ change_validation: latestValidation ? {
846
+ overall_compliance: latestValidation.overall_compliance,
847
+ last_validated: latestValidation.timestamp,
848
+ file_analyses: latestValidation.file_analyses || [],
849
+ human_readable_changes: latestValidation.summary?.human_readable_changes || [],
850
+ recommendations: latestValidation.recommendations || []
851
+ } : null,
852
+ implementation_summary: latestSummary ? {
853
+ period_start: latestSummary.period_start,
854
+ period_end: latestSummary.period_end,
855
+ files_changed_count: latestSummary.files_changed?.length || 0,
856
+ implementation_notes: latestSummary.implementation_notes || [],
857
+ compliance_overview: latestSummary.compliance_overview || {
858
+ total_files: 0,
859
+ compliant_files: 0,
860
+ deviation_files: 0,
861
+ unexpected_files: 0,
862
+ overall_status: 'unknown'
863
+ },
864
+ actionable_insights: latestSummary.actionable_insights || [],
865
+ git_commits_count: latestSummary.git_commits?.length || 0
866
+ } : null
867
+ });
868
+ }
869
+ }
870
+ // Get untracked changes
871
+ const untrackedChanges = fileMonitor.getUntrackedChanges();
872
+ const recentChanges = fileMonitor.getRecentChanges(50);
873
+ // Get recent notifications and convert them to dashboard format
874
+ const recentNotifications = notificationManager.getRecentNotifications(20);
875
+ const notificationChanges = recentNotifications.map(notification => ({
876
+ type: 'notification',
877
+ path: `[${notification.event}]`,
878
+ timestamp: notification.timestamp,
879
+ spec_id: notification.spec_id,
880
+ event: notification.event,
881
+ data: notification.data,
882
+ role: notification.data?.role || 'system'
883
+ }));
884
+ // Get MCP tool operations from all SPECs for recent activity
885
+ const mcpOperationChanges = [];
886
+ for (const spec of enhancedSpecs) {
887
+ const mcpOps = spec.timeline?.filter((item) => item.type === 'mcp_operation') || [];
888
+ mcpOperationChanges.push(...mcpOps.slice(-5).map((op) => ({
889
+ type: 'mcp_operation',
890
+ path: `[MCP] ${op.tool_name}`,
891
+ timestamp: op.timestamp,
892
+ spec_id: spec.spec_id,
893
+ event: op.tool_name,
894
+ tool_name: op.tool_name,
895
+ success: op.success,
896
+ duration_ms: op.duration_ms,
897
+ parameters: op.parameters,
898
+ role: 'mcp_system'
899
+ })));
900
+ }
901
+ // Combine file changes, notifications, and MCP operations, sort by timestamp
902
+ const allChanges = [
903
+ ...recentChanges.map((change) => ({ ...change, type: 'file' })),
904
+ ...notificationChanges,
905
+ ...mcpOperationChanges
906
+ ].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).slice(0, 50);
907
+ // Sort specs by updated_at descending (latest first)
908
+ const sortedSpecs = enhancedSpecs.sort((a, b) => new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime());
909
+ res.json({
910
+ specs: sortedSpecs,
911
+ untracked: untrackedChanges,
912
+ recent_changes: allChanges,
913
+ timestamp: new Date().toISOString()
914
+ });
915
+ }
916
+ catch (error) {
917
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
918
+ }
919
+ });
920
+ // Dashboard notification endpoints
921
+ app.post("/api/updates", async (req, res) => {
922
+ try {
923
+ const { spec_id, event, data, timestamp, enhanced_activity } = req.body;
924
+ if (!event) {
925
+ return res.status(400).json({ error: "Event is required" });
926
+ }
927
+ const notification = notificationManager.addNotification({
928
+ spec_id,
929
+ event,
930
+ data: data || {},
931
+ timestamp: timestamp || new Date().toISOString()
932
+ });
933
+ // If enhanced_activity is provided, save it to SPEC file
934
+ if (spec_id && enhanced_activity) {
935
+ try {
936
+ const specsDir = path.resolve(process.cwd(), ".bob/specs");
937
+ const specPath = path.join(specsDir, `${spec_id}.json`);
938
+ if (await fs.pathExists(specPath)) {
939
+ const specData = await fs.readJson(specPath);
940
+ // Initialize enhanced_activities if needed
941
+ if (!specData.enhanced_activities) {
942
+ specData.enhanced_activities = [];
943
+ }
944
+ // Add enhanced activity
945
+ specData.enhanced_activities.push(enhanced_activity);
946
+ specData.updated_at = new Date().toISOString();
947
+ // Save back to file
948
+ await fs.writeFile(specPath, JSON.stringify(specData, null, 2), "utf8");
949
+ }
950
+ }
951
+ catch (fileError) {
952
+ console.error(`Failed to save enhanced activity to SPEC ${spec_id}:`, fileError);
953
+ // Continue - notification was still saved
954
+ }
955
+ }
956
+ res.json({
957
+ status: "notification_received",
958
+ notification_id: notification.id
959
+ });
960
+ }
961
+ catch (error) {
962
+ res.status(500).json({
963
+ error: error instanceof Error ? error.message : String(error)
964
+ });
965
+ }
966
+ });
967
+ app.get("/api/notifications", (req, res) => {
968
+ try {
969
+ const limit = parseInt(req.query.limit) || 50;
970
+ const spec_id = req.query.spec_id;
971
+ let notifications;
972
+ if (spec_id) {
973
+ notifications = notificationManager.getNotificationsBySpec(spec_id, limit);
974
+ }
975
+ else {
976
+ notifications = notificationManager.getRecentNotifications(limit);
977
+ }
978
+ res.json({
979
+ notifications,
980
+ count: notifications.length,
981
+ timestamp: new Date().toISOString()
982
+ });
983
+ }
984
+ catch (error) {
985
+ res.status(500).json({
986
+ error: error instanceof Error ? error.message : String(error)
987
+ });
988
+ }
989
+ });
990
+ app.post("/api/notifications/cleanup", (req, res) => {
991
+ try {
992
+ const hours = parseInt(req.body.hours) || 24;
993
+ const removed = notificationManager.clearOldNotifications(hours);
994
+ res.json({
995
+ status: "cleanup_completed",
996
+ removed_count: removed
997
+ });
998
+ }
999
+ catch (error) {
1000
+ res.status(500).json({
1001
+ error: error instanceof Error ? error.message : String(error)
1002
+ });
1003
+ }
1004
+ });
1005
+ // SSE endpoint for real-time events
1006
+ app.get('/api/events/stream', (req, res) => {
1007
+ res.writeHead(200, {
1008
+ 'Content-Type': 'text/event-stream',
1009
+ 'Cache-Control': 'no-cache',
1010
+ 'Connection': 'keep-alive',
1011
+ 'Access-Control-Allow-Origin': '*'
1012
+ });
1013
+ // Send initial connection event
1014
+ res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: new Date().toISOString() })}\n\n`);
1015
+ // Subscribe to logical operations
1016
+ const handleOperation = (operation) => {
1017
+ res.write(`data: ${JSON.stringify(operation)}\n\n`);
1018
+ };
1019
+ mcpLogger.eventEmitter.on('logical_operation_start', handleOperation);
1020
+ mcpLogger.eventEmitter.on('logical_operation_update', handleOperation);
1021
+ mcpLogger.eventEmitter.on('logical_operation_complete', handleOperation);
1022
+ // Cleanup on disconnect
1023
+ req.on('close', () => {
1024
+ mcpLogger.eventEmitter.off('logical_operation_start', handleOperation);
1025
+ mcpLogger.eventEmitter.off('logical_operation_update', handleOperation);
1026
+ mcpLogger.eventEmitter.off('logical_operation_complete', handleOperation);
1027
+ });
1028
+ // Keep connection alive
1029
+ const keepAlive = setInterval(() => {
1030
+ res.write(': keepalive\n\n');
1031
+ }, 30000);
1032
+ req.on('close', () => clearInterval(keepAlive));
1033
+ });
1034
+ // Health check
1035
+ // MCP logs endpoint for activity timeline
1036
+ app.get("/api/mcp-logs", (req, res) => {
1037
+ const specId = req.query.spec_id;
1038
+ const logs = mcpLogger.getLogs(specId);
1039
+ res.json({ logs });
1040
+ });
1041
+ // Task Logger API Endpoints
1042
+ // GET /api/tasks - List all tasks
1043
+ app.get("/api/tasks", (req, res) => {
1044
+ try {
1045
+ const { priority, status, search } = req.query;
1046
+ let tasks;
1047
+ if (priority) {
1048
+ tasks = taskService.getTasksByPriority(priority);
1049
+ }
1050
+ else if (status) {
1051
+ tasks = taskService.getTasksByStatus(status);
1052
+ }
1053
+ else if (search) {
1054
+ tasks = taskService.searchTasks(search);
1055
+ }
1056
+ else {
1057
+ tasks = taskService.getAllTasks();
1058
+ }
1059
+ res.json(createSuccessResponse({
1060
+ tasks,
1061
+ count: tasks.length,
1062
+ total: taskService.getTaskCount()
1063
+ }));
1064
+ }
1065
+ catch (error) {
1066
+ console.error("[TaskLogger API] Error listing tasks:", error);
1067
+ res.status(500).json(createErrorResponse("Failed to list tasks", 500));
1068
+ }
1069
+ });
1070
+ // POST /api/tasks - Create new task
1071
+ app.post("/api/tasks", (req, res) => {
1072
+ try {
1073
+ const task = taskService.createTask(req.body);
1074
+ res.status(201).json(createSuccessResponse(task, "Task created successfully"));
1075
+ }
1076
+ catch (error) {
1077
+ console.error("[TaskLogger API] Error creating task:", error);
1078
+ if (error instanceof Error && error.message.includes("validation")) {
1079
+ res.status(400).json(createErrorResponse(error.message));
1080
+ }
1081
+ else {
1082
+ res.status(500).json(createErrorResponse("Failed to create task", 500));
1083
+ }
1084
+ }
1085
+ });
1086
+ // GET /api/tasks/:id - Get specific task
1087
+ app.get("/api/tasks/:id", (req, res) => {
1088
+ try {
1089
+ const task = taskService.getTaskById(req.params.id);
1090
+ if (!task) {
1091
+ return res.status(404).json(createErrorResponse("Task not found", 404));
1092
+ }
1093
+ res.json(createSuccessResponse(task));
1094
+ }
1095
+ catch (error) {
1096
+ console.error("[TaskLogger API] Error getting task:", error);
1097
+ res.status(500).json(createErrorResponse("Failed to get task", 500));
1098
+ }
1099
+ });
1100
+ // PUT /api/tasks/:id - Update task
1101
+ app.put("/api/tasks/:id", (req, res) => {
1102
+ try {
1103
+ const task = taskService.updateTask(req.params.id, req.body);
1104
+ if (!task) {
1105
+ return res.status(404).json(createErrorResponse("Task not found", 404));
1106
+ }
1107
+ res.json(createSuccessResponse(task, "Task updated successfully"));
1108
+ }
1109
+ catch (error) {
1110
+ console.error("[TaskLogger API] Error updating task:", error);
1111
+ if (error instanceof Error && error.message.includes("validation")) {
1112
+ res.status(400).json(createErrorResponse(error.message));
1113
+ }
1114
+ else {
1115
+ res.status(500).json(createErrorResponse("Failed to update task", 500));
1116
+ }
1117
+ }
1118
+ });
1119
+ // DELETE /api/tasks/:id - Delete task
1120
+ app.delete("/api/tasks/:id", (req, res) => {
1121
+ try {
1122
+ const deleted = taskService.deleteTask(req.params.id);
1123
+ if (!deleted) {
1124
+ return res.status(404).json(createErrorResponse("Task not found", 404));
1125
+ }
1126
+ res.json(createSuccessResponse(null, "Task deleted successfully"));
1127
+ }
1128
+ catch (error) {
1129
+ console.error("[TaskLogger API] Error deleting task:", error);
1130
+ res.status(500).json(createErrorResponse("Failed to delete task", 500));
1131
+ }
1132
+ });
1133
+ app.get("/api/git-diff", async (req, res) => {
1134
+ try {
1135
+ const { file, commit } = req.query;
1136
+ if (!file) {
1137
+ return res.status(400).json({ error: "File parameter is required" });
1138
+ }
1139
+ const gitDiff = await getFileGitDiff(file);
1140
+ if (gitDiff) {
1141
+ res.json({
1142
+ file: file,
1143
+ lines_added: gitDiff.linesAdded,
1144
+ lines_removed: gitDiff.linesRemoved,
1145
+ diff_summary: gitDiff.diffSummary,
1146
+ total_changes: gitDiff.linesAdded + gitDiff.linesRemoved
1147
+ });
1148
+ }
1149
+ else {
1150
+ res.json({
1151
+ file: file,
1152
+ lines_added: 0,
1153
+ lines_removed: 0,
1154
+ diff_summary: "",
1155
+ total_changes: 0
1156
+ });
1157
+ }
1158
+ }
1159
+ catch (error) {
1160
+ console.error("Error getting git diff:", error);
1161
+ res.status(500).json({ error: "Failed to get git diff" });
1162
+ }
1163
+ });
1164
+ // Serve MCP manifest at /.well-known/mcp/manifest.json
1165
+ app.get("/.well-known/mcp/manifest.json", async (req, res) => {
1166
+ try {
1167
+ const manifestPath = path.join(projectRoot, "public", ".well-known", "mcp", "manifest.json");
1168
+ if (!await fs.pathExists(manifestPath)) {
1169
+ return res.status(404).json({ error: "Manifest not found" });
1170
+ }
1171
+ const manifest = await fs.readJson(manifestPath);
1172
+ // Set appropriate headers for manifest serving
1173
+ res.setHeader('Content-Type', 'application/json');
1174
+ res.setHeader('Cache-Control', 'public, max-age=300'); // 5 minutes cache
1175
+ res.json(manifest);
1176
+ }
1177
+ catch (error) {
1178
+ console.error("Error serving manifest:", error);
1179
+ res.status(500).json({ error: "Failed to serve manifest" });
1180
+ }
1181
+ });
1182
+ app.get("/api/health", (req, res) => {
1183
+ res.json({ status: "ok", uptime: process.uptime() });
1184
+ });
1185
+ return app;
1186
+ }
1187
+ // Start server when run directly
1188
+ const isMainModule = () => {
1189
+ // Don't auto-start in test environments
1190
+ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
1191
+ return false;
1192
+ }
1193
+ // Check if import.meta is available
1194
+ if (typeof import.meta !== 'undefined' && import.meta.url) {
1195
+ return import.meta.url === `file://${process.argv[1]}`;
1196
+ }
1197
+ return false;
1198
+ };
1199
+ if (isMainModule()) {
1200
+ const app = createDashboardServer();
1201
+ const port = 4577; // Match the expected dashboard port
1202
+ app.listen(port, () => {
1203
+ console.log(`Bob's Workshop dashboard running on http://localhost:${port}`);
1204
+ });
1205
+ } // Test git diff functionality - timestamp: Sun 28 Sep 2025 19:38:38 IST
1206
+ //# sourceMappingURL=server.js.map