@magic-ingredients/tiny-brain-local 0.3.10
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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/core/console-logger.d.ts +30 -0
- package/dist/core/console-logger.d.ts.map +1 -0
- package/dist/core/console-logger.js +101 -0
- package/dist/core/console-logger.js.map +1 -0
- package/dist/core/file-logger.d.ts +40 -0
- package/dist/core/file-logger.d.ts.map +1 -0
- package/dist/core/file-logger.js +223 -0
- package/dist/core/file-logger.js.map +1 -0
- package/dist/core/mcp-server.d.ts +54 -0
- package/dist/core/mcp-server.d.ts.map +1 -0
- package/dist/core/mcp-server.js +295 -0
- package/dist/core/mcp-server.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/index.d.ts +39 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +5 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/memory/memory.prompt.d.ts +32 -0
- package/dist/prompts/memory/memory.prompt.d.ts.map +1 -0
- package/dist/prompts/memory/memory.prompt.js +204 -0
- package/dist/prompts/memory/memory.prompt.js.map +1 -0
- package/dist/prompts/persona/persona.prompt.d.ts +27 -0
- package/dist/prompts/persona/persona.prompt.d.ts.map +1 -0
- package/dist/prompts/persona/persona.prompt.js +592 -0
- package/dist/prompts/persona/persona.prompt.js.map +1 -0
- package/dist/prompts/planning/planning.prompt.d.ts +56 -0
- package/dist/prompts/planning/planning.prompt.d.ts.map +1 -0
- package/dist/prompts/planning/planning.prompt.js +1016 -0
- package/dist/prompts/planning/planning.prompt.js.map +1 -0
- package/dist/prompts/prompt-registry.d.ts +25 -0
- package/dist/prompts/prompt-registry.d.ts.map +1 -0
- package/dist/prompts/prompt-registry.js +68 -0
- package/dist/prompts/prompt-registry.js.map +1 -0
- package/dist/prompts/thinking/thinking.prompt.d.ts +29 -0
- package/dist/prompts/thinking/thinking.prompt.d.ts.map +1 -0
- package/dist/prompts/thinking/thinking.prompt.js +171 -0
- package/dist/prompts/thinking/thinking.prompt.js.map +1 -0
- package/dist/services/UpdateService.d.ts +29 -0
- package/dist/services/UpdateService.d.ts.map +1 -0
- package/dist/services/UpdateService.js +132 -0
- package/dist/services/UpdateService.js.map +1 -0
- package/dist/services/plan-watcher.service.d.ts +143 -0
- package/dist/services/plan-watcher.service.d.ts.map +1 -0
- package/dist/services/plan-watcher.service.js +914 -0
- package/dist/services/plan-watcher.service.js.map +1 -0
- package/dist/storage/local-filesystem-adapter.d.ts +39 -0
- package/dist/storage/local-filesystem-adapter.d.ts.map +1 -0
- package/dist/storage/local-filesystem-adapter.js +208 -0
- package/dist/storage/local-filesystem-adapter.js.map +1 -0
- package/dist/storage/storage-path-builder.d.ts +14 -0
- package/dist/storage/storage-path-builder.d.ts.map +1 -0
- package/dist/storage/storage-path-builder.js +43 -0
- package/dist/storage/storage-path-builder.js.map +1 -0
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +12 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/tools/analyse-request/analyse-request.tool.d.ts +8 -0
- package/dist/tools/analyse-request/analyse-request.tool.d.ts.map +1 -0
- package/dist/tools/analyse-request/analyse-request.tool.js +120 -0
- package/dist/tools/analyse-request/analyse-request.tool.js.map +1 -0
- package/dist/tools/index.d.ts +69 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +24 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory/memory.tool.d.ts +15 -0
- package/dist/tools/memory/memory.tool.d.ts.map +1 -0
- package/dist/tools/memory/memory.tool.js +110 -0
- package/dist/tools/memory/memory.tool.js.map +1 -0
- package/dist/tools/persona/as.tool.d.ts +25 -0
- package/dist/tools/persona/as.tool.d.ts.map +1 -0
- package/dist/tools/persona/as.tool.js +294 -0
- package/dist/tools/persona/as.tool.js.map +1 -0
- package/dist/tools/persona/persona.tool.d.ts +8 -0
- package/dist/tools/persona/persona.tool.d.ts.map +1 -0
- package/dist/tools/persona/persona.tool.js +193 -0
- package/dist/tools/persona/persona.tool.js.map +1 -0
- package/dist/tools/plan/plan.tool.d.ts +18 -0
- package/dist/tools/plan/plan.tool.d.ts.map +1 -0
- package/dist/tools/plan/plan.tool.js +643 -0
- package/dist/tools/plan/plan.tool.js.map +1 -0
- package/dist/tools/strategy/strategy.tool.d.ts +13 -0
- package/dist/tools/strategy/strategy.tool.d.ts.map +1 -0
- package/dist/tools/strategy/strategy.tool.js +199 -0
- package/dist/tools/strategy/strategy.tool.js.map +1 -0
- package/dist/tools/thinking/thinking.tool.d.ts +13 -0
- package/dist/tools/thinking/thinking.tool.d.ts.map +1 -0
- package/dist/tools/thinking/thinking.tool.js +226 -0
- package/dist/tools/thinking/thinking.tool.js.map +1 -0
- package/dist/tools/tool-registry.d.ts +20 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/tools/tool-registry.js +61 -0
- package/dist/tools/tool-registry.js.map +1 -0
- package/dist/tools/update/update.tool.d.ts +15 -0
- package/dist/tools/update/update.tool.d.ts.map +1 -0
- package/dist/tools/update/update.tool.js +86 -0
- package/dist/tools/update/update.tool.js.map +1 -0
- package/dist/tools/validate-response/validate-response.tool.d.ts +13 -0
- package/dist/tools/validate-response/validate-response.tool.d.ts.map +1 -0
- package/dist/tools/validate-response/validate-response.tool.js +142 -0
- package/dist/tools/validate-response/validate-response.tool.js.map +1 -0
- package/dist/types/request-context.d.ts +7 -0
- package/dist/types/request-context.d.ts.map +1 -0
- package/dist/types/request-context.js +7 -0
- package/dist/types/request-context.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Watcher Service
|
|
3
|
+
*
|
|
4
|
+
* Provides a web dashboard for monitoring and viewing plans in real-time.
|
|
5
|
+
* Implements singleton pattern to ensure only one watcher runs at a time.
|
|
6
|
+
*/
|
|
7
|
+
import * as http from 'http';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import open from 'better-opn';
|
|
13
|
+
import { PlanningService } from '@magic-ingredients/tiny-brain-core';
|
|
14
|
+
/**
|
|
15
|
+
* Singleton instance tracking
|
|
16
|
+
*/
|
|
17
|
+
let runningInstance = null;
|
|
18
|
+
/**
|
|
19
|
+
* Plan Watcher Service
|
|
20
|
+
*/
|
|
21
|
+
export class PlanWatcherService {
|
|
22
|
+
server = null;
|
|
23
|
+
watcher = null;
|
|
24
|
+
watchInterval = null;
|
|
25
|
+
port = 0;
|
|
26
|
+
plans = new Map();
|
|
27
|
+
eventEmitter = new EventEmitter();
|
|
28
|
+
activePlanId = null;
|
|
29
|
+
showAll = false;
|
|
30
|
+
currentViewType = undefined;
|
|
31
|
+
context;
|
|
32
|
+
logger;
|
|
33
|
+
activeConnections = new Set();
|
|
34
|
+
boundOnPersonaChanged = null;
|
|
35
|
+
currentPersona = 'default';
|
|
36
|
+
constructor(context) {
|
|
37
|
+
this.context = context;
|
|
38
|
+
this.logger = context.logger;
|
|
39
|
+
this.logger.debug('PlanWatcher constructor called');
|
|
40
|
+
this.logger.debug(`context.getCurrentActivePersona exists? ${!!context.getCurrentActivePersona}`);
|
|
41
|
+
this.logger.debug(`context.activePersona: ${JSON.stringify(context.activePersona)}`);
|
|
42
|
+
// Get current persona from context if available
|
|
43
|
+
// First try the callback, then activePersona, then default
|
|
44
|
+
if (context.getCurrentActivePersona) {
|
|
45
|
+
const callbackResult = context.getCurrentActivePersona();
|
|
46
|
+
this.logger.debug(`getCurrentActivePersona() returned: ${callbackResult}`);
|
|
47
|
+
this.currentPersona = callbackResult || 'default';
|
|
48
|
+
}
|
|
49
|
+
else if (context.activePersona?.id) {
|
|
50
|
+
this.logger.debug(`Using activePersona.id: ${context.activePersona.id}`);
|
|
51
|
+
this.currentPersona = context.activePersona.id;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
this.logger.debug('Falling back to default persona');
|
|
55
|
+
this.currentPersona = 'default';
|
|
56
|
+
}
|
|
57
|
+
this.logger.info(`PlanWatcherService initialized with persona: ${this.currentPersona}`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start the plan watcher dashboard
|
|
61
|
+
*/
|
|
62
|
+
async start(options = {}) {
|
|
63
|
+
try {
|
|
64
|
+
// Check if already running
|
|
65
|
+
if (this.isRunning()) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: new Error('Plan watcher is already running on port ' + this.port)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Check singleton
|
|
72
|
+
if (runningInstance && runningInstance !== this) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
error: new Error('Another plan watcher instance is already running')
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Update current persona before starting
|
|
79
|
+
this.logger.debug('Getting current persona before start...');
|
|
80
|
+
const previousPersona = this.currentPersona;
|
|
81
|
+
this.currentPersona = this.getCurrentPersonaId();
|
|
82
|
+
this.logger.debug(`Persona changed from ${previousPersona} to ${this.currentPersona}`);
|
|
83
|
+
this.logger.info(`Starting plan watcher with persona: ${this.currentPersona}`);
|
|
84
|
+
// Register our callback to be notified of persona changes
|
|
85
|
+
if (this.context.personaChangeListeners) {
|
|
86
|
+
// Store the bound function so we can unregister it later
|
|
87
|
+
this.boundOnPersonaChanged = this.onPersonaChanged.bind(this);
|
|
88
|
+
const lengthBefore = this.context.personaChangeListeners.length;
|
|
89
|
+
this.context.personaChangeListeners.push(this.boundOnPersonaChanged);
|
|
90
|
+
const lengthAfter = this.context.personaChangeListeners.length;
|
|
91
|
+
this.logger.info(`Watcher registered callback. Listeners before: ${lengthBefore}, after: ${lengthAfter}`);
|
|
92
|
+
console.log(`🔍 Watcher registered callback. Array went from ${lengthBefore} to ${lengthAfter} listeners`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.logger.warn('No personaChangeListeners array in context - callbacks not supported');
|
|
96
|
+
console.log('⚠️ No personaChangeListeners array in context');
|
|
97
|
+
}
|
|
98
|
+
// Set options
|
|
99
|
+
this.port = options.port || 8765;
|
|
100
|
+
this.showAll = options.showAll || false;
|
|
101
|
+
const autoOpen = options.autoOpen !== undefined
|
|
102
|
+
? options.autoOpen
|
|
103
|
+
: true; // Default to true
|
|
104
|
+
// Load initial plans
|
|
105
|
+
const plansLoaded = await this.loadPlans();
|
|
106
|
+
// Create HTTP server
|
|
107
|
+
this.server = this.createHttpServer();
|
|
108
|
+
// Start server
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
const serverInstance = this.server;
|
|
111
|
+
if (!serverInstance)
|
|
112
|
+
throw new Error('Server not created');
|
|
113
|
+
serverInstance.on('error', (err) => {
|
|
114
|
+
if (err.code === 'EADDRINUSE') {
|
|
115
|
+
reject(new Error(`Port ${this.port} is already in use`));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
reject(err);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
serverInstance.listen(this.port, () => {
|
|
122
|
+
this.logger.info(`Plan watcher started on port ${this.port}`);
|
|
123
|
+
resolve();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
// Set singleton instance
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
128
|
+
runningInstance = this;
|
|
129
|
+
// Start watching for changes
|
|
130
|
+
await this.startWatching();
|
|
131
|
+
const url = `http://localhost:${this.port}`;
|
|
132
|
+
// Open browser if autoOpen is enabled
|
|
133
|
+
if (autoOpen) {
|
|
134
|
+
try {
|
|
135
|
+
await open(url);
|
|
136
|
+
this.logger.info(`Opened browser at ${url}`);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.logger.warn('Failed to open browser automatically:', error);
|
|
140
|
+
// Don't fail the whole operation if browser opening fails
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
data: {
|
|
146
|
+
url,
|
|
147
|
+
port: this.port,
|
|
148
|
+
autoOpen,
|
|
149
|
+
plansLoaded
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
error: new Error(`Failed to start plan watcher: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Stop the plan watcher dashboard
|
|
162
|
+
*/
|
|
163
|
+
async stop() {
|
|
164
|
+
try {
|
|
165
|
+
if (!this.isRunning()) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: new Error('Plan watcher is not running')
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Unregister our callback
|
|
172
|
+
if (this.context.personaChangeListeners && this.boundOnPersonaChanged) {
|
|
173
|
+
const index = this.context.personaChangeListeners.indexOf(this.boundOnPersonaChanged);
|
|
174
|
+
if (index > -1) {
|
|
175
|
+
this.context.personaChangeListeners.splice(index, 1);
|
|
176
|
+
}
|
|
177
|
+
this.boundOnPersonaChanged = null;
|
|
178
|
+
}
|
|
179
|
+
// Stop file watcher
|
|
180
|
+
if (this.watcher) {
|
|
181
|
+
this.watcher.close();
|
|
182
|
+
this.watcher = null;
|
|
183
|
+
}
|
|
184
|
+
// Stop polling interval
|
|
185
|
+
if (this.watchInterval) {
|
|
186
|
+
clearInterval(this.watchInterval);
|
|
187
|
+
this.watchInterval = null;
|
|
188
|
+
}
|
|
189
|
+
// Close all active SSE connections first
|
|
190
|
+
// This is important to prevent hanging on server.close()
|
|
191
|
+
for (const connection of this.activeConnections) {
|
|
192
|
+
try {
|
|
193
|
+
connection.end();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Connection might already be closed
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.activeConnections.clear();
|
|
200
|
+
// Stop HTTP server
|
|
201
|
+
if (this.server) {
|
|
202
|
+
const serverToClose = this.server;
|
|
203
|
+
this.server = null; // Clear reference immediately
|
|
204
|
+
// Use a timeout to prevent indefinite hanging
|
|
205
|
+
await Promise.race([
|
|
206
|
+
new Promise((resolve) => {
|
|
207
|
+
serverToClose.close(() => {
|
|
208
|
+
this.logger.info('Plan watcher stopped');
|
|
209
|
+
resolve();
|
|
210
|
+
});
|
|
211
|
+
}),
|
|
212
|
+
new Promise((resolve) => {
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
this.logger.warn('Force closing server after timeout');
|
|
215
|
+
resolve();
|
|
216
|
+
}, 1000); // 1 second timeout
|
|
217
|
+
})
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
// Clear singleton if it's this instance
|
|
221
|
+
if (runningInstance === this) {
|
|
222
|
+
runningInstance = null;
|
|
223
|
+
}
|
|
224
|
+
// Clear data
|
|
225
|
+
this.plans.clear();
|
|
226
|
+
this.port = 0;
|
|
227
|
+
return { success: true, data: undefined };
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: new Error(`Failed to stop plan watcher: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if the watcher is running
|
|
238
|
+
*/
|
|
239
|
+
isRunning() {
|
|
240
|
+
return this.server !== null && this.port > 0;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get the URL of the running watcher
|
|
244
|
+
*/
|
|
245
|
+
getUrl() {
|
|
246
|
+
if (!this.isRunning()) {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: new Error('Plan watcher is not running')
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
data: `http://localhost:${this.port}`
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get the current active persona ID
|
|
259
|
+
* Uses the callback to always get the latest persona from MCP server
|
|
260
|
+
*/
|
|
261
|
+
getCurrentPersonaId() {
|
|
262
|
+
this.logger.debug('getCurrentPersonaId called');
|
|
263
|
+
// Use callback if available, otherwise fallback to context snapshot
|
|
264
|
+
if (this.context.getCurrentActivePersona) {
|
|
265
|
+
const personaId = this.context.getCurrentActivePersona();
|
|
266
|
+
this.logger.debug(`Callback returned: ${personaId}`);
|
|
267
|
+
if (personaId) {
|
|
268
|
+
return personaId;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
this.logger.debug('No getCurrentActivePersona callback available');
|
|
273
|
+
}
|
|
274
|
+
// Fallback to activePersona from context
|
|
275
|
+
if (this.context.activePersona?.id) {
|
|
276
|
+
this.logger.debug(`Using context.activePersona.id: ${this.context.activePersona.id}`);
|
|
277
|
+
return this.context.activePersona.id;
|
|
278
|
+
}
|
|
279
|
+
// Last resort: use stored currentPersona or default
|
|
280
|
+
this.logger.debug(`Using stored currentPersona: ${this.currentPersona || 'default'}`);
|
|
281
|
+
return this.currentPersona || 'default';
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Handle persona change notification from MCP server
|
|
285
|
+
* This is called when 'as' tool or 'become' prompt changes the persona
|
|
286
|
+
*/
|
|
287
|
+
async onPersonaChanged(newPersonaId) {
|
|
288
|
+
this.logger.info(`🔔 CALLBACK TRIGGERED: Persona changed to ${newPersonaId}, reloading plans...`);
|
|
289
|
+
console.log(`🔔 CALLBACK TRIGGERED: Persona changed to ${newPersonaId}`);
|
|
290
|
+
// Update current persona
|
|
291
|
+
this.currentPersona = newPersonaId;
|
|
292
|
+
// Clear current plans
|
|
293
|
+
this.plans.clear();
|
|
294
|
+
// Reload plans with new persona
|
|
295
|
+
const plansLoaded = await this.loadPlans();
|
|
296
|
+
this.logger.info(`Reloaded ${plansLoaded} plans for persona ${newPersonaId}`);
|
|
297
|
+
// Emit update event to refresh dashboard
|
|
298
|
+
this.eventEmitter.emit('update', {
|
|
299
|
+
type: 'persona-changed',
|
|
300
|
+
personaId: newPersonaId
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Load plans from storage using PlanningService
|
|
305
|
+
*/
|
|
306
|
+
async loadPlans() {
|
|
307
|
+
try {
|
|
308
|
+
// Create a PlanningService instance with the current context
|
|
309
|
+
const planningService = new PlanningService(this.context);
|
|
310
|
+
// List plans based on current view type
|
|
311
|
+
// Default to 'active' if no view type is set, unless showAll is true
|
|
312
|
+
const plans = await planningService.listPlans(this.currentViewType ? { type: this.currentViewType } :
|
|
313
|
+
this.showAll ? {} : { type: 'active' });
|
|
314
|
+
// Get the currently active plan ID
|
|
315
|
+
const activePlan = await planningService.getActivePlan();
|
|
316
|
+
this.activePlanId = activePlan?.id || null;
|
|
317
|
+
// Don't use a fallback - let the frontend handle selection
|
|
318
|
+
this.logger.info(`[PlanWatcher] Active plan ID: ${this.activePlanId || 'none'}, Active plan title: ${activePlan?.title || 'none'}`);
|
|
319
|
+
if (!plans || plans.length === 0) {
|
|
320
|
+
this.logger.info('No plans found');
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
// Clear existing plans and load the new ones
|
|
324
|
+
this.plans.clear();
|
|
325
|
+
for (const plan of plans) {
|
|
326
|
+
// Store the plan with its id as the key
|
|
327
|
+
this.plans.set(plan.id, plan);
|
|
328
|
+
}
|
|
329
|
+
return this.plans.size;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
this.logger.error('Failed to load plans:', error);
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Start watching for plan file changes
|
|
338
|
+
*/
|
|
339
|
+
async startWatching() {
|
|
340
|
+
// Set up polling interval to check for changes
|
|
341
|
+
const POLL_INTERVAL = 2000; // Check every 2 seconds
|
|
342
|
+
this.watchInterval = setInterval(async () => {
|
|
343
|
+
await this.checkForChanges();
|
|
344
|
+
}, POLL_INTERVAL);
|
|
345
|
+
// Initial snapshot
|
|
346
|
+
await this.updateFileSnapshot();
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Check for changes in plan files without reloading data
|
|
350
|
+
*/
|
|
351
|
+
async checkForChanges() {
|
|
352
|
+
try {
|
|
353
|
+
// Store previous plan state for comparison
|
|
354
|
+
const previousPlanIds = new Set(this.plans.keys());
|
|
355
|
+
const previousPlans = new Map();
|
|
356
|
+
for (const [id, plan] of this.plans) {
|
|
357
|
+
const typedPlan = plan;
|
|
358
|
+
if (typedPlan.lastUpdated) {
|
|
359
|
+
previousPlans.set(id, typedPlan.lastUpdated);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Create a temporary planning service to check current state without modifying this.plans
|
|
363
|
+
const planningService = new PlanningService(this.context);
|
|
364
|
+
const currentPlans = await planningService.listPlans(this.currentViewType ? { type: this.currentViewType } :
|
|
365
|
+
this.showAll ? {} : { type: 'active' });
|
|
366
|
+
// Build current state for comparison
|
|
367
|
+
const currentPlanIds = new Set(currentPlans.map(p => p.id));
|
|
368
|
+
const currentPlanData = new Map();
|
|
369
|
+
for (const plan of currentPlans) {
|
|
370
|
+
if (plan.lastUpdated) {
|
|
371
|
+
currentPlanData.set(plan.id, plan.lastUpdated);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Detect changes without reloading
|
|
375
|
+
let hasChanges = false;
|
|
376
|
+
// Check if plan count changed
|
|
377
|
+
if (previousPlanIds.size !== currentPlanIds.size) {
|
|
378
|
+
hasChanges = true;
|
|
379
|
+
this.logger.info(`Plan count changed: ${previousPlanIds.size} -> ${currentPlanIds.size}`);
|
|
380
|
+
}
|
|
381
|
+
// Check for added or removed plans
|
|
382
|
+
if (!hasChanges) {
|
|
383
|
+
for (const id of previousPlanIds) {
|
|
384
|
+
if (!currentPlanIds.has(id)) {
|
|
385
|
+
hasChanges = true;
|
|
386
|
+
this.logger.info(`Plan removed: ${id}`);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const id of currentPlanIds) {
|
|
391
|
+
if (!previousPlanIds.has(id)) {
|
|
392
|
+
hasChanges = true;
|
|
393
|
+
this.logger.info(`Plan added: ${id}`);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// Check for modified plans (different lastUpdated)
|
|
399
|
+
if (!hasChanges) {
|
|
400
|
+
for (const [id, currentUpdated] of currentPlanData) {
|
|
401
|
+
const previousUpdated = previousPlans.get(id);
|
|
402
|
+
if (!previousUpdated || previousUpdated !== currentUpdated) {
|
|
403
|
+
hasChanges = true;
|
|
404
|
+
this.logger.info(`Plan modified: ${id} (${previousUpdated} -> ${currentUpdated})`);
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Only reload and emit SSE event if changes detected
|
|
410
|
+
if (hasChanges) {
|
|
411
|
+
this.logger.info('Changes detected - reloading plans and emitting update event');
|
|
412
|
+
// Reload our cached plans with the new data
|
|
413
|
+
await this.loadPlans();
|
|
414
|
+
// Emit SSE event to notify dashboard
|
|
415
|
+
this.eventEmitter.emit('update', {
|
|
416
|
+
type: 'update',
|
|
417
|
+
changeType: 'modified'
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
this.logger.debug('No changes detected - skipping reload and SSE event');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
this.logger.error('Error checking for plan changes:', error);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Update the file snapshot (deprecated - kept for compatibility)
|
|
430
|
+
*/
|
|
431
|
+
async updateFileSnapshot() {
|
|
432
|
+
// No longer needed as we use PlanningService directly
|
|
433
|
+
// Just load plans to ensure we have the latest data
|
|
434
|
+
await this.loadPlans();
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Create the HTTP server for the dashboard
|
|
438
|
+
*/
|
|
439
|
+
createHttpServer() {
|
|
440
|
+
return http.createServer((req, res) => {
|
|
441
|
+
const url = new globalThis.URL(req.url || '/', `http://localhost:${this.port}`);
|
|
442
|
+
// Add CORS headers for all requests
|
|
443
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
444
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
445
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept');
|
|
446
|
+
// Handle preflight requests
|
|
447
|
+
if (req.method === 'OPTIONS') {
|
|
448
|
+
res.writeHead(200);
|
|
449
|
+
res.end();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Route handling
|
|
453
|
+
if (url.pathname === '/') {
|
|
454
|
+
this.serveDashboard(res);
|
|
455
|
+
}
|
|
456
|
+
else if (url.pathname === '/api/plans') {
|
|
457
|
+
// Support both old 'archived' param and new 'type' param
|
|
458
|
+
const typeParam = url.searchParams.get('type');
|
|
459
|
+
const archivedParam = url.searchParams.get('archived');
|
|
460
|
+
let planType;
|
|
461
|
+
if (typeParam === 'active' || typeParam === 'archived') {
|
|
462
|
+
planType = typeParam;
|
|
463
|
+
}
|
|
464
|
+
else if (archivedParam === 'true') {
|
|
465
|
+
planType = 'archived'; // Backward compatibility
|
|
466
|
+
}
|
|
467
|
+
else if (archivedParam === 'false') {
|
|
468
|
+
planType = 'active'; // Backward compatibility
|
|
469
|
+
}
|
|
470
|
+
// If no param or invalid param, planType remains undefined (returns all)
|
|
471
|
+
this.servePlansApi(res, planType);
|
|
472
|
+
}
|
|
473
|
+
else if (url.pathname.startsWith('/api/plan/')) {
|
|
474
|
+
const pathParts = url.pathname.substring('/api/plan/'.length).split('/');
|
|
475
|
+
const planId = pathParts[0];
|
|
476
|
+
const action = pathParts[1];
|
|
477
|
+
// Handle plan management actions
|
|
478
|
+
if (req.method === 'POST' && action) {
|
|
479
|
+
this.handlePlanAction(req, res, planId, action);
|
|
480
|
+
}
|
|
481
|
+
else if (req.method === 'DELETE' && !action) {
|
|
482
|
+
this.handlePlanDelete(req, res, planId);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
// Default GET request for plan details
|
|
486
|
+
this.servePlanApi(planId, url, res);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else if (url.pathname === '/api/persona') {
|
|
490
|
+
this.servePersonaApi(req, res);
|
|
491
|
+
}
|
|
492
|
+
else if (url.pathname === '/events') {
|
|
493
|
+
this.serveEventStream(req, res);
|
|
494
|
+
}
|
|
495
|
+
else if (url.pathname.startsWith('/assets/') || url.pathname.endsWith('.js') || url.pathname.endsWith('.css')) {
|
|
496
|
+
// Serve static assets for React dashboard
|
|
497
|
+
this.serveStaticAsset(url.pathname, res);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
// For client-side routing, serve index.html for non-API routes
|
|
501
|
+
const dashboardPath = this.getReactDashboardPath();
|
|
502
|
+
if (dashboardPath && fs.existsSync(dashboardPath)) {
|
|
503
|
+
this.serveReactDashboard(res);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
res.writeHead(404);
|
|
507
|
+
res.end('Not Found');
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Serve the main dashboard HTML
|
|
514
|
+
*/
|
|
515
|
+
serveDashboard(res) {
|
|
516
|
+
// Check if React build exists
|
|
517
|
+
const dashboardPath = this.getReactDashboardPath();
|
|
518
|
+
if (dashboardPath && fs.existsSync(dashboardPath)) {
|
|
519
|
+
this.serveReactDashboard(res);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
// Fallback to legacy HTML dashboard
|
|
523
|
+
const html = this.loadDashboardHtml();
|
|
524
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
525
|
+
res.end(html);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Serve the plans list API
|
|
530
|
+
*/
|
|
531
|
+
async servePlansApi(res, planType) {
|
|
532
|
+
// Update view type for background polling (but only after we load the new data)
|
|
533
|
+
this.showAll = planType === 'archived' || planType === undefined;
|
|
534
|
+
// Always clear and reload plans with the correct filter for API calls
|
|
535
|
+
this.plans.clear();
|
|
536
|
+
// Use existing context
|
|
537
|
+
// Load plans based on type (undefined means all)
|
|
538
|
+
const planningService = new PlanningService(this.context);
|
|
539
|
+
console.log(`[servePlansApi] Requesting plans with type: ${planType}`);
|
|
540
|
+
const plans = await planningService.listPlans(planType ? { type: planType } : {});
|
|
541
|
+
console.log(`[servePlansApi] Retrieved ${plans.length} plans:`, plans.map(p => ({ id: p.id, status: p.status })));
|
|
542
|
+
// Store the loaded plans (PlanningService already set the correct status based on folder)
|
|
543
|
+
for (const plan of plans) {
|
|
544
|
+
this.plans.set(plan.id, plan);
|
|
545
|
+
}
|
|
546
|
+
// NOW update the view type for background polling to prevent false change detection
|
|
547
|
+
this.currentViewType = planType;
|
|
548
|
+
// Get the currently active plan ID (if any)
|
|
549
|
+
const activePlan = await planningService.getActivePlan();
|
|
550
|
+
this.activePlanId = activePlan?.id || null;
|
|
551
|
+
const plansList = Array.from(this.plans.entries()).map(([id, plan]) => {
|
|
552
|
+
const typedPlan = plan;
|
|
553
|
+
const isActive = id === this.activePlanId;
|
|
554
|
+
return {
|
|
555
|
+
id,
|
|
556
|
+
name: typedPlan.title || typedPlan.name || 'Unnamed Plan',
|
|
557
|
+
status: typedPlan.status || 'unknown',
|
|
558
|
+
phaseCount: typedPlan.phases?.length || 0,
|
|
559
|
+
progress: this.calculateProgress(plan),
|
|
560
|
+
updatedAt: typedPlan.updatedAt,
|
|
561
|
+
isCurrentlyActive: isActive,
|
|
562
|
+
// Add debug info directly to each plan
|
|
563
|
+
_debug: {
|
|
564
|
+
planId: id,
|
|
565
|
+
activePlanId: this.activePlanId,
|
|
566
|
+
isMatch: id === this.activePlanId
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
const response = {
|
|
571
|
+
plans: plansList,
|
|
572
|
+
persona: this.currentPersona,
|
|
573
|
+
activePlanId: this.activePlanId,
|
|
574
|
+
debug: {
|
|
575
|
+
activePlanIdBeforeLoad: this.activePlanId,
|
|
576
|
+
plansCount: this.plans.size,
|
|
577
|
+
showAll: this.showAll
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
581
|
+
res.end(JSON.stringify(response));
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Serve a specific plan API
|
|
585
|
+
*/
|
|
586
|
+
async servePlanApi(planId, url, res) {
|
|
587
|
+
const plan = this.plans.get(planId);
|
|
588
|
+
if (!plan) {
|
|
589
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
590
|
+
res.end(JSON.stringify({ error: 'Plan not found' }));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// Check if we want the formatted report
|
|
594
|
+
const format = url.searchParams.get('format');
|
|
595
|
+
if (format === 'report') {
|
|
596
|
+
try {
|
|
597
|
+
// Use PlanningService to generate the status report
|
|
598
|
+
const planningService = new PlanningService(this.context);
|
|
599
|
+
const report = await planningService.generateStatusReport(plan);
|
|
600
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
601
|
+
res.end(JSON.stringify({
|
|
602
|
+
report,
|
|
603
|
+
format: 'markdown'
|
|
604
|
+
}));
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
this.logger.error('Failed to generate plan report:', error);
|
|
608
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
609
|
+
res.end(JSON.stringify({ error: 'Failed to generate report' }));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// Return raw JSON as before
|
|
614
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
615
|
+
res.end(JSON.stringify(plan));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Serve the persona API (GET current only)
|
|
620
|
+
*/
|
|
621
|
+
servePersonaApi(req, res) {
|
|
622
|
+
if (req.method === 'GET') {
|
|
623
|
+
// Return current persona from MCP server via callback
|
|
624
|
+
const personaId = this.getCurrentPersonaId();
|
|
625
|
+
this.logger.debug(`/api/persona requested, returning: ${personaId}`);
|
|
626
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
627
|
+
res.end(JSON.stringify({
|
|
628
|
+
personaId: personaId
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
// Persona changes are handled by MCP server via 'as' tool or 'become' prompt
|
|
633
|
+
// The dashboard will detect changes automatically via polling
|
|
634
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
635
|
+
res.end(JSON.stringify({
|
|
636
|
+
error: 'Method not allowed. Use the "as" tool or "become" prompt to change personas.'
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Handle plan management actions (archive, unarchive, activate)
|
|
642
|
+
*/
|
|
643
|
+
async handlePlanAction(_req, res, planId, action) {
|
|
644
|
+
try {
|
|
645
|
+
// Use existing context
|
|
646
|
+
const planningService = new PlanningService(this.context);
|
|
647
|
+
let success = false;
|
|
648
|
+
let errorMessage = '';
|
|
649
|
+
try {
|
|
650
|
+
switch (action) {
|
|
651
|
+
case 'archive':
|
|
652
|
+
await planningService.archivePlan({ planId, reason: 'Archived via dashboard' });
|
|
653
|
+
success = true;
|
|
654
|
+
break;
|
|
655
|
+
case 'unarchive':
|
|
656
|
+
await planningService.unarchivePlan(planId);
|
|
657
|
+
success = true;
|
|
658
|
+
break;
|
|
659
|
+
case 'activate':
|
|
660
|
+
this.logger.info(`Attempting to switch to plan: ${planId} for persona: ${this.currentPersona}`);
|
|
661
|
+
await planningService.switchToPlan(planId);
|
|
662
|
+
success = true;
|
|
663
|
+
this.logger.info(`Successfully switched to plan: ${planId}`);
|
|
664
|
+
break;
|
|
665
|
+
default:
|
|
666
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
667
|
+
res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
success = false;
|
|
673
|
+
errorMessage = error instanceof Error ? error.message : 'Operation failed';
|
|
674
|
+
this.logger.error(`Error in plan action ${action} for ${planId}: ${errorMessage}`, error);
|
|
675
|
+
}
|
|
676
|
+
if (success) {
|
|
677
|
+
// Reload plans to reflect changes
|
|
678
|
+
await this.loadPlans();
|
|
679
|
+
// Send success response
|
|
680
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
681
|
+
res.end(JSON.stringify({ success: true, message: `Plan ${action}d successfully` }));
|
|
682
|
+
// Emit update event for real-time sync
|
|
683
|
+
this.eventEmitter.emit('update', { type: 'update', planId, action });
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
687
|
+
res.end(JSON.stringify({ error: errorMessage || `Failed to ${action} plan` }));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
this.logger.error(`Error handling plan action ${action} for ${planId}:`, error);
|
|
692
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
693
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Handle plan deletion
|
|
698
|
+
*/
|
|
699
|
+
async handlePlanDelete(_req, res, planId) {
|
|
700
|
+
try {
|
|
701
|
+
// Use existing context
|
|
702
|
+
const planningService = new PlanningService(this.context);
|
|
703
|
+
// Check if plan exists and is archived
|
|
704
|
+
const plan = this.plans.get(planId);
|
|
705
|
+
if (!plan) {
|
|
706
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
707
|
+
res.end(JSON.stringify({ error: 'Plan not found' }));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
// Type assertion since we know the structure
|
|
711
|
+
const planData = plan;
|
|
712
|
+
if (planData.type !== 'archived') {
|
|
713
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
714
|
+
res.end(JSON.stringify({ error: 'Only archived plans can be deleted' }));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const result = await planningService.deletePlan(planId);
|
|
719
|
+
if (result) {
|
|
720
|
+
// Reload plans to reflect changes
|
|
721
|
+
await this.loadPlans();
|
|
722
|
+
// Send success response
|
|
723
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
724
|
+
res.end(JSON.stringify({ success: true, message: 'Plan deleted successfully' }));
|
|
725
|
+
// Emit update event for real-time sync
|
|
726
|
+
this.eventEmitter.emit('update', { type: 'update', planId, action: 'delete' });
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
730
|
+
res.end(JSON.stringify({ error: 'Failed to delete plan' }));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to delete plan';
|
|
735
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
736
|
+
res.end(JSON.stringify({ error: errorMessage }));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
this.logger.error(`Error deleting plan ${planId}:`, error);
|
|
741
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
742
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Serve Server-Sent Events for real-time updates
|
|
747
|
+
*/
|
|
748
|
+
serveEventStream(req, res) {
|
|
749
|
+
res.writeHead(200, {
|
|
750
|
+
'Content-Type': 'text/event-stream',
|
|
751
|
+
'Cache-Control': 'no-cache',
|
|
752
|
+
'Connection': 'keep-alive',
|
|
753
|
+
'Access-Control-Allow-Origin': '*',
|
|
754
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
755
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
756
|
+
});
|
|
757
|
+
// Track this connection
|
|
758
|
+
this.activeConnections.add(res);
|
|
759
|
+
// Send initial connection message
|
|
760
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
761
|
+
// Keep-alive ping every 30 seconds
|
|
762
|
+
const keepAlive = setInterval(() => {
|
|
763
|
+
res.write(':ping\n\n');
|
|
764
|
+
}, 30000);
|
|
765
|
+
// Listen for plan updates
|
|
766
|
+
const updateHandler = (data) => {
|
|
767
|
+
try {
|
|
768
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
// Connection might be closed
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
this.eventEmitter.on('update', updateHandler);
|
|
775
|
+
// Clean up on client disconnect
|
|
776
|
+
req.on('close', () => {
|
|
777
|
+
clearInterval(keepAlive);
|
|
778
|
+
this.eventEmitter.off('update', updateHandler);
|
|
779
|
+
this.activeConnections.delete(res);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Calculate progress percentage for a plan
|
|
784
|
+
*/
|
|
785
|
+
calculateProgress(plan) {
|
|
786
|
+
const typedPlan = plan;
|
|
787
|
+
if (!typedPlan.phases || typedPlan.phases.length === 0)
|
|
788
|
+
return 0;
|
|
789
|
+
const completedPhases = typedPlan.phases.filter((p) => p.status === 'completed').length;
|
|
790
|
+
return Math.round((completedPhases / typedPlan.phases.length) * 100);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Load and process the dashboard HTML
|
|
794
|
+
*/
|
|
795
|
+
loadDashboardHtml() {
|
|
796
|
+
try {
|
|
797
|
+
// Get the directory of the current module
|
|
798
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
799
|
+
const __dirname = path.dirname(__filename);
|
|
800
|
+
// Read the dashboard HTML file
|
|
801
|
+
const htmlPath = path.join(__dirname, 'dashboard.html');
|
|
802
|
+
let html = fs.readFileSync(htmlPath, 'utf-8');
|
|
803
|
+
// Replace template variables
|
|
804
|
+
html = html.replace(/{{CURRENT_PERSONA}}/g, this.currentPersona);
|
|
805
|
+
return html;
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
this.logger.error('Failed to load dashboard HTML:', error);
|
|
809
|
+
// Fallback to a simple error page
|
|
810
|
+
return this.generateFallbackHtml();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Generate fallback HTML if dashboard.html cannot be loaded
|
|
815
|
+
*/
|
|
816
|
+
generateFallbackHtml() {
|
|
817
|
+
return `<!DOCTYPE html>
|
|
818
|
+
<html>
|
|
819
|
+
<head><title>Error</title></head>
|
|
820
|
+
<body>
|
|
821
|
+
<h1>Dashboard Error</h1>
|
|
822
|
+
<p>Failed to load dashboard. Please check the logs.</p>
|
|
823
|
+
<p>Current persona: ${this.currentPersona}</p>
|
|
824
|
+
</body>
|
|
825
|
+
</html>`;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Get the path to the React dashboard build
|
|
829
|
+
*/
|
|
830
|
+
getReactDashboardPath() {
|
|
831
|
+
try {
|
|
832
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
833
|
+
const __dirname = path.dirname(__filename);
|
|
834
|
+
// Try production build location first
|
|
835
|
+
const prodPath = path.join(__dirname, '..', '..', 'dashboard');
|
|
836
|
+
if (fs.existsSync(path.join(prodPath, 'index.html'))) {
|
|
837
|
+
return prodPath;
|
|
838
|
+
}
|
|
839
|
+
// Try development build location
|
|
840
|
+
const devPath = path.join(__dirname, '..', '..', '..', 'tiny-brain-dashboard', 'dist');
|
|
841
|
+
if (fs.existsSync(path.join(devPath, 'index.html'))) {
|
|
842
|
+
return devPath;
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
catch (error) {
|
|
847
|
+
this.logger.error('Failed to locate React dashboard:', error);
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Serve the React dashboard
|
|
853
|
+
*/
|
|
854
|
+
serveReactDashboard(res) {
|
|
855
|
+
const dashboardPath = this.getReactDashboardPath();
|
|
856
|
+
if (!dashboardPath) {
|
|
857
|
+
this.serveDashboard(res);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const indexPath = path.join(dashboardPath, 'index.html');
|
|
861
|
+
const html = fs.readFileSync(indexPath, 'utf-8');
|
|
862
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
863
|
+
res.end(html);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Serve static assets for React dashboard
|
|
867
|
+
*/
|
|
868
|
+
serveStaticAsset(urlPath, res) {
|
|
869
|
+
const dashboardPath = this.getReactDashboardPath();
|
|
870
|
+
if (!dashboardPath) {
|
|
871
|
+
res.writeHead(404);
|
|
872
|
+
res.end('Not Found');
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
// Remove leading slash and resolve path
|
|
876
|
+
const assetPath = path.join(dashboardPath, urlPath.substring(1));
|
|
877
|
+
// Security: Ensure the path is within the dashboard directory
|
|
878
|
+
if (!assetPath.startsWith(dashboardPath)) {
|
|
879
|
+
res.writeHead(403);
|
|
880
|
+
res.end('Forbidden');
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (!fs.existsSync(assetPath)) {
|
|
884
|
+
res.writeHead(404);
|
|
885
|
+
res.end('Not Found');
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
// Determine content type
|
|
889
|
+
const ext = path.extname(assetPath).toLowerCase();
|
|
890
|
+
const contentTypes = {
|
|
891
|
+
'.html': 'text/html',
|
|
892
|
+
'.js': 'application/javascript',
|
|
893
|
+
'.css': 'text/css',
|
|
894
|
+
'.json': 'application/json',
|
|
895
|
+
'.png': 'image/png',
|
|
896
|
+
'.jpg': 'image/jpeg',
|
|
897
|
+
'.gif': 'image/gif',
|
|
898
|
+
'.svg': 'image/svg+xml',
|
|
899
|
+
'.ico': 'image/x-icon',
|
|
900
|
+
};
|
|
901
|
+
const contentType = contentTypes[ext] || 'application/octet-stream';
|
|
902
|
+
try {
|
|
903
|
+
const content = fs.readFileSync(assetPath);
|
|
904
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
905
|
+
res.end(content);
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
this.logger.error('Failed to serve static asset:', error);
|
|
909
|
+
res.writeHead(500);
|
|
910
|
+
res.end('Internal Server Error');
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
//# sourceMappingURL=plan-watcher.service.js.map
|