bobs-workshop 0.3.2 → 3.1.0

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 (200) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +199 -210
  3. package/bin/bobs-workshop.js +109 -0
  4. package/config/agents.json +27 -0
  5. package/dist/plugins/bobs-workshop.js +34 -0
  6. package/dist/tools/background-agent/cancel.d.ts +3 -0
  7. package/dist/tools/background-agent/cancel.d.ts.map +1 -0
  8. package/dist/tools/background-agent/cancel.js +52 -0
  9. package/dist/tools/background-agent/concurrency.d.ts +15 -0
  10. package/dist/tools/background-agent/concurrency.d.ts.map +1 -0
  11. package/dist/tools/background-agent/concurrency.js +61 -0
  12. package/dist/tools/background-agent/index.d.ts +8 -0
  13. package/dist/tools/background-agent/index.d.ts.map +1 -0
  14. package/dist/tools/background-agent/index.js +7 -0
  15. package/dist/tools/background-agent/launch.d.ts +6 -0
  16. package/dist/tools/background-agent/launch.d.ts.map +1 -0
  17. package/dist/tools/background-agent/launch.js +33 -0
  18. package/dist/tools/background-agent/list.d.ts +7 -0
  19. package/dist/tools/background-agent/list.d.ts.map +1 -0
  20. package/dist/tools/background-agent/list.js +40 -0
  21. package/dist/tools/background-agent/manager.d.ts +29 -0
  22. package/dist/tools/background-agent/manager.d.ts.map +1 -0
  23. package/dist/tools/background-agent/manager.js +377 -0
  24. package/dist/tools/background-agent/output.d.ts +3 -0
  25. package/dist/tools/background-agent/output.d.ts.map +1 -0
  26. package/dist/tools/background-agent/output.js +41 -0
  27. package/dist/tools/background-agent/types.d.ts +46 -0
  28. package/dist/tools/background-agent/types.d.ts.map +1 -0
  29. package/dist/tools/background-agent/types.js +1 -0
  30. package/dist/tools/index.d.ts +9 -0
  31. package/dist/tools/index.d.ts.map +1 -0
  32. package/dist/tools/index.js +8 -0
  33. package/dist/tools/manual/index.d.ts +3 -0
  34. package/dist/tools/manual/index.d.ts.map +1 -0
  35. package/dist/tools/manual/index.js +2 -0
  36. package/dist/tools/manual/manual-update.d.ts +4 -0
  37. package/dist/tools/manual/manual-update.d.ts.map +1 -0
  38. package/dist/tools/manual/manual-update.js +190 -0
  39. package/dist/tools/manual/verify-manual.d.ts +4 -0
  40. package/dist/tools/manual/verify-manual.d.ts.map +1 -0
  41. package/dist/tools/manual/verify-manual.js +46 -0
  42. package/package.json +35 -67
  43. package/postinstall.js +190 -0
  44. package/src/agents/alice.md +466 -0
  45. package/src/agents/bob-rev.md +493 -0
  46. package/src/agents/bob-send.md +277 -0
  47. package/src/agents/bob.md +442 -0
  48. package/src/agents/trace.md +451 -0
  49. package/src/plugins/bobs-workshop.ts +45 -0
  50. package/src/skills/api-patterns/SKILL.md +376 -0
  51. package/src/skills/architecture/SKILL.md +271 -0
  52. package/src/skills/bobs-workshop/performance/icon.svg +3 -0
  53. package/src/skills/brainstorming/SKILL.md +210 -0
  54. package/src/skills/clean-code/SKILL.md +151 -0
  55. package/src/skills/code-review-checklist/SKILL.md +220 -0
  56. package/src/skills/database-design/SKILL.md +271 -0
  57. package/src/skills/exploration/SKILL.md +257 -0
  58. package/src/skills/frontend-ui-ux/SKILL.md +78 -0
  59. package/src/skills/git-master/SKILL.md +1105 -0
  60. package/src/skills/performance/SKILL.md +144 -0
  61. package/src/skills/performance/icon.svg +3 -0
  62. package/src/skills/plan-writing/SKILL.md +225 -0
  63. package/src/skills/security/SKILL.md +410 -0
  64. package/src/skills/simplification/SKILL.md +238 -0
  65. package/src/skills/systematic-debugging/SKILL.md +175 -0
  66. package/src/skills/testing-patterns/SKILL.md +305 -0
  67. package/src/skills/verification/SKILL.md +286 -0
  68. package/src/tools/background-agent/cancel.ts +67 -0
  69. package/src/tools/background-agent/concurrency.ts +71 -0
  70. package/src/tools/background-agent/index.ts +7 -0
  71. package/src/tools/background-agent/launch.ts +39 -0
  72. package/src/tools/background-agent/list.ts +50 -0
  73. package/src/tools/background-agent/manager.ts +455 -0
  74. package/src/tools/background-agent/output.ts +57 -0
  75. package/src/tools/background-agent/types.ts +55 -0
  76. package/src/tools/index.ts +8 -0
  77. package/src/tools/manual/index.ts +2 -0
  78. package/src/tools/manual/manual-update.ts +197 -0
  79. package/src/tools/manual/verify-manual.ts +55 -0
  80. package/uninstall.js +64 -0
  81. package/Claude.md +0 -162
  82. package/bin/bobs-mcp-server.js +0 -11
  83. package/bin/bobs-mcp.js +0 -130
  84. package/dist/api/taskLogger.js +0 -106
  85. package/dist/api/taskLogger.js.map +0 -1
  86. package/dist/cli/checker.js +0 -401
  87. package/dist/cli/checker.js.map +0 -1
  88. package/dist/cli/cleanup.js +0 -131
  89. package/dist/cli/cleanup.js.map +0 -1
  90. package/dist/cli/debug.js +0 -157
  91. package/dist/cli/debug.js.map +0 -1
  92. package/dist/cli/health.js +0 -97
  93. package/dist/cli/health.js.map +0 -1
  94. package/dist/cli/setup.js +0 -81
  95. package/dist/cli/setup.js.map +0 -1
  96. package/dist/cli/workshop.js +0 -42
  97. package/dist/cli/workshop.js.map +0 -1
  98. package/dist/dashboard/server.js +0 -1203
  99. package/dist/dashboard/server.js.map +0 -1
  100. package/dist/index.js +0 -960
  101. package/dist/index.js.map +0 -1
  102. package/dist/prompts/architect.js +0 -221
  103. package/dist/prompts/architect.js.map +0 -1
  104. package/dist/prompts/debugger.js +0 -257
  105. package/dist/prompts/debugger.js.map +0 -1
  106. package/dist/prompts/engineer.js +0 -249
  107. package/dist/prompts/engineer.js.map +0 -1
  108. package/dist/prompts/orchestrator.js +0 -304
  109. package/dist/prompts/orchestrator.js.map +0 -1
  110. package/dist/prompts/reviewer.js +0 -289
  111. package/dist/prompts/reviewer.js.map +0 -1
  112. package/dist/services/activitySummarizer.js +0 -388
  113. package/dist/services/activitySummarizer.js.map +0 -1
  114. package/dist/services/changeValidator.js +0 -396
  115. package/dist/services/changeValidator.js.map +0 -1
  116. package/dist/services/claudeOrchestrator.js +0 -343
  117. package/dist/services/claudeOrchestrator.js.map +0 -1
  118. package/dist/services/fileMonitor.js +0 -250
  119. package/dist/services/fileMonitor.js.map +0 -1
  120. package/dist/services/implementationSummarizer.js +0 -306
  121. package/dist/services/implementationSummarizer.js.map +0 -1
  122. package/dist/services/liveMonitor.js +0 -315
  123. package/dist/services/liveMonitor.js.map +0 -1
  124. package/dist/services/mcpAuditLogger.js +0 -104
  125. package/dist/services/mcpAuditLogger.js.map +0 -1
  126. package/dist/services/mcpLogger.js +0 -223
  127. package/dist/services/mcpLogger.js.map +0 -1
  128. package/dist/services/tmuxManager.js +0 -541
  129. package/dist/services/tmuxManager.js.map +0 -1
  130. package/dist/tools/approvalTools.js +0 -244
  131. package/dist/tools/approvalTools.js.map +0 -1
  132. package/dist/tools/autoDebugger.js +0 -147
  133. package/dist/tools/autoDebugger.js.map +0 -1
  134. package/dist/tools/cleanupService.js +0 -221
  135. package/dist/tools/cleanupService.js.map +0 -1
  136. package/dist/tools/dashboardTools.js +0 -342
  137. package/dist/tools/dashboardTools.js.map +0 -1
  138. package/dist/tools/developmentNudges.js +0 -336
  139. package/dist/tools/developmentNudges.js.map +0 -1
  140. package/dist/tools/gitTools.js +0 -741
  141. package/dist/tools/gitTools.js.map +0 -1
  142. package/dist/tools/orchestratorTools.js +0 -832
  143. package/dist/tools/orchestratorTools.js.map +0 -1
  144. package/dist/tools/searchCache.js +0 -64
  145. package/dist/tools/searchCache.js.map +0 -1
  146. package/dist/tools/searchTools.js +0 -1107
  147. package/dist/tools/searchTools.js.map +0 -1
  148. package/dist/tools/semgrep-patterns.js +0 -296
  149. package/dist/tools/semgrep-patterns.js.map +0 -1
  150. package/dist/tools/specTools.js +0 -332
  151. package/dist/tools/specTools.js.map +0 -1
  152. package/dist/tools/structural/__tests__/orchestrator.test.js +0 -61
  153. package/dist/tools/structural/__tests__/orchestrator.test.js.map +0 -1
  154. package/dist/tools/structural/cache.js +0 -226
  155. package/dist/tools/structural/cache.js.map +0 -1
  156. package/dist/tools/structural/engines/python/index.js +0 -118
  157. package/dist/tools/structural/engines/python/index.js.map +0 -1
  158. package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js +0 -97
  159. package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js.map +0 -1
  160. package/dist/tools/structural/engines/typescript/analyzer.js +0 -433
  161. package/dist/tools/structural/engines/typescript/analyzer.js.map +0 -1
  162. package/dist/tools/structural/engines/typescript/index.js +0 -381
  163. package/dist/tools/structural/engines/typescript/index.js.map +0 -1
  164. package/dist/tools/structural/engines/typescript/utils.js +0 -279
  165. package/dist/tools/structural/engines/typescript/utils.js.map +0 -1
  166. package/dist/tools/structural/index.js +0 -248
  167. package/dist/tools/structural/index.js.map +0 -1
  168. package/dist/tools/structural/types.js +0 -18
  169. package/dist/tools/structural/types.js.map +0 -1
  170. package/dist/tools/tmuxTools.js +0 -100
  171. package/dist/tools/tmuxTools.js.map +0 -1
  172. package/dist/tools/workRecorder.js +0 -215
  173. package/dist/tools/workRecorder.js.map +0 -1
  174. package/dist/tools/worktreeTools.js +0 -705
  175. package/dist/tools/worktreeTools.js.map +0 -1
  176. package/dist/utils/__tests__/integration.test.js +0 -57
  177. package/dist/utils/__tests__/integration.test.js.map +0 -1
  178. package/dist/utils/__tests__/serverDetection.test.js +0 -151
  179. package/dist/utils/__tests__/serverDetection.test.js.map +0 -1
  180. package/dist/utils/errorHandling.js +0 -336
  181. package/dist/utils/errorHandling.js.map +0 -1
  182. package/dist/utils/processManager.js +0 -172
  183. package/dist/utils/processManager.js.map +0 -1
  184. package/dist/utils/reliability.js +0 -263
  185. package/dist/utils/reliability.js.map +0 -1
  186. package/dist/utils/responseFormatter.js +0 -250
  187. package/dist/utils/responseFormatter.js.map +0 -1
  188. package/dist/utils/serverDetection.js +0 -133
  189. package/dist/utils/serverDetection.js.map +0 -1
  190. package/dist/utils/specMigration.js +0 -105
  191. package/dist/utils/specMigration.js.map +0 -1
  192. package/dist/validation/schemas.js +0 -299
  193. package/dist/validation/schemas.js.map +0 -1
  194. package/public/.well-known/mcp/manifest.json +0 -473
  195. package/public/index.html +0 -3157
  196. package/public/index.html.backup +0 -2805
  197. package/public/index.html.backup2 +0 -1292
  198. package/scripts/cleanup-system-logs.ts +0 -121
  199. package/scripts/init-workspace.js +0 -63
  200. package/scripts/install-search-tools.js +0 -116
@@ -1,1203 +0,0 @@
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
- // Note: Enhanced activities are persisted via bob.manual.update only
934
- // This endpoint is just for real-time UI notifications
935
- res.json({
936
- status: "notification_received",
937
- notification_id: notification.id
938
- });
939
- }
940
- catch (error) {
941
- res.status(500).json({
942
- error: error instanceof Error ? error.message : String(error)
943
- });
944
- }
945
- });
946
- app.get("/api/notifications", (req, res) => {
947
- try {
948
- const limit = parseInt(req.query.limit) || 50;
949
- const spec_id = req.query.spec_id;
950
- let notifications;
951
- if (spec_id) {
952
- notifications = notificationManager.getNotificationsBySpec(spec_id, limit);
953
- }
954
- else {
955
- notifications = notificationManager.getRecentNotifications(limit);
956
- }
957
- res.json({
958
- notifications,
959
- count: notifications.length,
960
- timestamp: new Date().toISOString()
961
- });
962
- }
963
- catch (error) {
964
- res.status(500).json({
965
- error: error instanceof Error ? error.message : String(error)
966
- });
967
- }
968
- });
969
- app.post("/api/notifications/cleanup", (req, res) => {
970
- try {
971
- const hours = parseInt(req.body.hours) || 24;
972
- const removed = notificationManager.clearOldNotifications(hours);
973
- res.json({
974
- status: "cleanup_completed",
975
- removed_count: removed
976
- });
977
- }
978
- catch (error) {
979
- res.status(500).json({
980
- error: error instanceof Error ? error.message : String(error)
981
- });
982
- }
983
- });
984
- // SSE endpoint for real-time events
985
- app.get('/api/events/stream', (req, res) => {
986
- res.writeHead(200, {
987
- 'Content-Type': 'text/event-stream',
988
- 'Cache-Control': 'no-cache',
989
- 'Connection': 'keep-alive',
990
- 'Access-Control-Allow-Origin': '*'
991
- });
992
- // Send initial connection event
993
- res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: new Date().toISOString() })}\n\n`);
994
- // Subscribe to logical operations
995
- const handleOperation = (operation) => {
996
- res.write(`data: ${JSON.stringify(operation)}\n\n`);
997
- };
998
- mcpLogger.eventEmitter.on('logical_operation_start', handleOperation);
999
- mcpLogger.eventEmitter.on('logical_operation_update', handleOperation);
1000
- mcpLogger.eventEmitter.on('logical_operation_complete', handleOperation);
1001
- // Cleanup on disconnect
1002
- req.on('close', () => {
1003
- mcpLogger.eventEmitter.off('logical_operation_start', handleOperation);
1004
- mcpLogger.eventEmitter.off('logical_operation_update', handleOperation);
1005
- mcpLogger.eventEmitter.off('logical_operation_complete', handleOperation);
1006
- });
1007
- // Keep connection alive
1008
- const keepAlive = setInterval(() => {
1009
- res.write(': keepalive\n\n');
1010
- }, 30000);
1011
- req.on('close', () => clearInterval(keepAlive));
1012
- });
1013
- // Health check
1014
- // MCP logs endpoint for activity timeline
1015
- app.get("/api/mcp-logs", (req, res) => {
1016
- const specId = req.query.spec_id;
1017
- const logs = mcpLogger.getLogs(specId);
1018
- res.json({ logs });
1019
- });
1020
- // Task Logger API Endpoints
1021
- // GET /api/tasks - List all tasks
1022
- app.get("/api/tasks", (req, res) => {
1023
- try {
1024
- const { priority, status, search } = req.query;
1025
- let tasks;
1026
- if (priority) {
1027
- tasks = taskService.getTasksByPriority(priority);
1028
- }
1029
- else if (status) {
1030
- tasks = taskService.getTasksByStatus(status);
1031
- }
1032
- else if (search) {
1033
- tasks = taskService.searchTasks(search);
1034
- }
1035
- else {
1036
- tasks = taskService.getAllTasks();
1037
- }
1038
- res.json(createSuccessResponse({
1039
- tasks,
1040
- count: tasks.length,
1041
- total: taskService.getTaskCount()
1042
- }));
1043
- }
1044
- catch (error) {
1045
- console.error("[TaskLogger API] Error listing tasks:", error);
1046
- res.status(500).json(createErrorResponse("Failed to list tasks", 500));
1047
- }
1048
- });
1049
- // POST /api/tasks - Create new task
1050
- app.post("/api/tasks", (req, res) => {
1051
- try {
1052
- const task = taskService.createTask(req.body);
1053
- res.status(201).json(createSuccessResponse(task, "Task created successfully"));
1054
- }
1055
- catch (error) {
1056
- console.error("[TaskLogger API] Error creating task:", error);
1057
- if (error instanceof Error && error.message.includes("validation")) {
1058
- res.status(400).json(createErrorResponse(error.message));
1059
- }
1060
- else {
1061
- res.status(500).json(createErrorResponse("Failed to create task", 500));
1062
- }
1063
- }
1064
- });
1065
- // GET /api/tasks/:id - Get specific task
1066
- app.get("/api/tasks/:id", (req, res) => {
1067
- try {
1068
- const task = taskService.getTaskById(req.params.id);
1069
- if (!task) {
1070
- return res.status(404).json(createErrorResponse("Task not found", 404));
1071
- }
1072
- res.json(createSuccessResponse(task));
1073
- }
1074
- catch (error) {
1075
- console.error("[TaskLogger API] Error getting task:", error);
1076
- res.status(500).json(createErrorResponse("Failed to get task", 500));
1077
- }
1078
- });
1079
- // PUT /api/tasks/:id - Update task
1080
- app.put("/api/tasks/:id", (req, res) => {
1081
- try {
1082
- const task = taskService.updateTask(req.params.id, req.body);
1083
- if (!task) {
1084
- return res.status(404).json(createErrorResponse("Task not found", 404));
1085
- }
1086
- res.json(createSuccessResponse(task, "Task updated successfully"));
1087
- }
1088
- catch (error) {
1089
- console.error("[TaskLogger API] Error updating task:", error);
1090
- if (error instanceof Error && error.message.includes("validation")) {
1091
- res.status(400).json(createErrorResponse(error.message));
1092
- }
1093
- else {
1094
- res.status(500).json(createErrorResponse("Failed to update task", 500));
1095
- }
1096
- }
1097
- });
1098
- // DELETE /api/tasks/:id - Delete task
1099
- app.delete("/api/tasks/:id", (req, res) => {
1100
- try {
1101
- const deleted = taskService.deleteTask(req.params.id);
1102
- if (!deleted) {
1103
- return res.status(404).json(createErrorResponse("Task not found", 404));
1104
- }
1105
- res.json(createSuccessResponse(null, "Task deleted successfully"));
1106
- }
1107
- catch (error) {
1108
- console.error("[TaskLogger API] Error deleting task:", error);
1109
- res.status(500).json(createErrorResponse("Failed to delete task", 500));
1110
- }
1111
- });
1112
- app.get("/api/git-diff", async (req, res) => {
1113
- try {
1114
- const { file, commit } = req.query;
1115
- if (!file) {
1116
- return res.status(400).json({ error: "File parameter is required" });
1117
- }
1118
- const gitDiff = await getFileGitDiff(file);
1119
- if (gitDiff) {
1120
- res.json({
1121
- file: file,
1122
- lines_added: gitDiff.linesAdded,
1123
- lines_removed: gitDiff.linesRemoved,
1124
- diff_summary: gitDiff.diffSummary,
1125
- total_changes: gitDiff.linesAdded + gitDiff.linesRemoved
1126
- });
1127
- }
1128
- else {
1129
- res.json({
1130
- file: file,
1131
- lines_added: 0,
1132
- lines_removed: 0,
1133
- diff_summary: "",
1134
- total_changes: 0
1135
- });
1136
- }
1137
- }
1138
- catch (error) {
1139
- console.error("Error getting git diff:", error);
1140
- res.status(500).json({ error: "Failed to get git diff" });
1141
- }
1142
- });
1143
- // Serve MCP manifest at /.well-known/mcp/manifest.json
1144
- app.get("/.well-known/mcp/manifest.json", async (req, res) => {
1145
- try {
1146
- const manifestPath = path.join(projectRoot, "public", ".well-known", "mcp", "manifest.json");
1147
- if (!await fs.pathExists(manifestPath)) {
1148
- return res.status(404).json({ error: "Manifest not found" });
1149
- }
1150
- const manifest = await fs.readJson(manifestPath);
1151
- // Set appropriate headers for manifest serving
1152
- res.setHeader('Content-Type', 'application/json');
1153
- res.setHeader('Cache-Control', 'public, max-age=300'); // 5 minutes cache
1154
- res.json(manifest);
1155
- }
1156
- catch (error) {
1157
- console.error("Error serving manifest:", error);
1158
- res.status(500).json({ error: "Failed to serve manifest" });
1159
- }
1160
- });
1161
- // Project info endpoint - returns current repository name based on process.cwd()
1162
- app.get("/api/project-info", (req, res) => {
1163
- try {
1164
- const cwd = process.cwd();
1165
- const repoName = path.basename(cwd);
1166
- res.json({
1167
- repoName,
1168
- workingDirectory: cwd
1169
- });
1170
- }
1171
- catch (error) {
1172
- res.status(500).json({
1173
- error: error instanceof Error ? error.message : String(error),
1174
- repoName: 'unknown',
1175
- workingDirectory: process.cwd()
1176
- });
1177
- }
1178
- });
1179
- app.get("/api/health", (req, res) => {
1180
- res.json({ status: "ok", uptime: process.uptime() });
1181
- });
1182
- return app;
1183
- }
1184
- // Start server when run directly
1185
- const isMainModule = () => {
1186
- // Don't auto-start in test environments
1187
- if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
1188
- return false;
1189
- }
1190
- // Check if import.meta is available
1191
- if (typeof import.meta !== 'undefined' && import.meta.url) {
1192
- return import.meta.url === `file://${process.argv[1]}`;
1193
- }
1194
- return false;
1195
- };
1196
- if (isMainModule()) {
1197
- const app = createDashboardServer();
1198
- const port = 4577; // Match the expected dashboard port
1199
- app.listen(port, () => {
1200
- console.log(`Bob's Workshop dashboard running on http://localhost:${port}`);
1201
- });
1202
- } // Test git diff functionality - timestamp: Sun 28 Sep 2025 19:38:38 IST
1203
- //# sourceMappingURL=server.js.map