@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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/core/console-logger.d.ts +30 -0
  4. package/dist/core/console-logger.d.ts.map +1 -0
  5. package/dist/core/console-logger.js +101 -0
  6. package/dist/core/console-logger.js.map +1 -0
  7. package/dist/core/file-logger.d.ts +40 -0
  8. package/dist/core/file-logger.d.ts.map +1 -0
  9. package/dist/core/file-logger.js +223 -0
  10. package/dist/core/file-logger.js.map +1 -0
  11. package/dist/core/mcp-server.d.ts +54 -0
  12. package/dist/core/mcp-server.d.ts.map +1 -0
  13. package/dist/core/mcp-server.js +295 -0
  14. package/dist/core/mcp-server.js.map +1 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +37 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/prompts/index.d.ts +39 -0
  20. package/dist/prompts/index.d.ts.map +1 -0
  21. package/dist/prompts/index.js +5 -0
  22. package/dist/prompts/index.js.map +1 -0
  23. package/dist/prompts/memory/memory.prompt.d.ts +32 -0
  24. package/dist/prompts/memory/memory.prompt.d.ts.map +1 -0
  25. package/dist/prompts/memory/memory.prompt.js +204 -0
  26. package/dist/prompts/memory/memory.prompt.js.map +1 -0
  27. package/dist/prompts/persona/persona.prompt.d.ts +27 -0
  28. package/dist/prompts/persona/persona.prompt.d.ts.map +1 -0
  29. package/dist/prompts/persona/persona.prompt.js +592 -0
  30. package/dist/prompts/persona/persona.prompt.js.map +1 -0
  31. package/dist/prompts/planning/planning.prompt.d.ts +56 -0
  32. package/dist/prompts/planning/planning.prompt.d.ts.map +1 -0
  33. package/dist/prompts/planning/planning.prompt.js +1016 -0
  34. package/dist/prompts/planning/planning.prompt.js.map +1 -0
  35. package/dist/prompts/prompt-registry.d.ts +25 -0
  36. package/dist/prompts/prompt-registry.d.ts.map +1 -0
  37. package/dist/prompts/prompt-registry.js +68 -0
  38. package/dist/prompts/prompt-registry.js.map +1 -0
  39. package/dist/prompts/thinking/thinking.prompt.d.ts +29 -0
  40. package/dist/prompts/thinking/thinking.prompt.d.ts.map +1 -0
  41. package/dist/prompts/thinking/thinking.prompt.js +171 -0
  42. package/dist/prompts/thinking/thinking.prompt.js.map +1 -0
  43. package/dist/services/UpdateService.d.ts +29 -0
  44. package/dist/services/UpdateService.d.ts.map +1 -0
  45. package/dist/services/UpdateService.js +132 -0
  46. package/dist/services/UpdateService.js.map +1 -0
  47. package/dist/services/plan-watcher.service.d.ts +143 -0
  48. package/dist/services/plan-watcher.service.d.ts.map +1 -0
  49. package/dist/services/plan-watcher.service.js +914 -0
  50. package/dist/services/plan-watcher.service.js.map +1 -0
  51. package/dist/storage/local-filesystem-adapter.d.ts +39 -0
  52. package/dist/storage/local-filesystem-adapter.d.ts.map +1 -0
  53. package/dist/storage/local-filesystem-adapter.js +208 -0
  54. package/dist/storage/local-filesystem-adapter.js.map +1 -0
  55. package/dist/storage/storage-path-builder.d.ts +14 -0
  56. package/dist/storage/storage-path-builder.d.ts.map +1 -0
  57. package/dist/storage/storage-path-builder.js +43 -0
  58. package/dist/storage/storage-path-builder.js.map +1 -0
  59. package/dist/test-setup.d.ts +2 -0
  60. package/dist/test-setup.d.ts.map +1 -0
  61. package/dist/test-setup.js +12 -0
  62. package/dist/test-setup.js.map +1 -0
  63. package/dist/tools/analyse-request/analyse-request.tool.d.ts +8 -0
  64. package/dist/tools/analyse-request/analyse-request.tool.d.ts.map +1 -0
  65. package/dist/tools/analyse-request/analyse-request.tool.js +120 -0
  66. package/dist/tools/analyse-request/analyse-request.tool.js.map +1 -0
  67. package/dist/tools/index.d.ts +69 -0
  68. package/dist/tools/index.d.ts.map +1 -0
  69. package/dist/tools/index.js +24 -0
  70. package/dist/tools/index.js.map +1 -0
  71. package/dist/tools/memory/memory.tool.d.ts +15 -0
  72. package/dist/tools/memory/memory.tool.d.ts.map +1 -0
  73. package/dist/tools/memory/memory.tool.js +110 -0
  74. package/dist/tools/memory/memory.tool.js.map +1 -0
  75. package/dist/tools/persona/as.tool.d.ts +25 -0
  76. package/dist/tools/persona/as.tool.d.ts.map +1 -0
  77. package/dist/tools/persona/as.tool.js +294 -0
  78. package/dist/tools/persona/as.tool.js.map +1 -0
  79. package/dist/tools/persona/persona.tool.d.ts +8 -0
  80. package/dist/tools/persona/persona.tool.d.ts.map +1 -0
  81. package/dist/tools/persona/persona.tool.js +193 -0
  82. package/dist/tools/persona/persona.tool.js.map +1 -0
  83. package/dist/tools/plan/plan.tool.d.ts +18 -0
  84. package/dist/tools/plan/plan.tool.d.ts.map +1 -0
  85. package/dist/tools/plan/plan.tool.js +643 -0
  86. package/dist/tools/plan/plan.tool.js.map +1 -0
  87. package/dist/tools/strategy/strategy.tool.d.ts +13 -0
  88. package/dist/tools/strategy/strategy.tool.d.ts.map +1 -0
  89. package/dist/tools/strategy/strategy.tool.js +199 -0
  90. package/dist/tools/strategy/strategy.tool.js.map +1 -0
  91. package/dist/tools/thinking/thinking.tool.d.ts +13 -0
  92. package/dist/tools/thinking/thinking.tool.d.ts.map +1 -0
  93. package/dist/tools/thinking/thinking.tool.js +226 -0
  94. package/dist/tools/thinking/thinking.tool.js.map +1 -0
  95. package/dist/tools/tool-registry.d.ts +20 -0
  96. package/dist/tools/tool-registry.d.ts.map +1 -0
  97. package/dist/tools/tool-registry.js +61 -0
  98. package/dist/tools/tool-registry.js.map +1 -0
  99. package/dist/tools/update/update.tool.d.ts +15 -0
  100. package/dist/tools/update/update.tool.d.ts.map +1 -0
  101. package/dist/tools/update/update.tool.js +86 -0
  102. package/dist/tools/update/update.tool.js.map +1 -0
  103. package/dist/tools/validate-response/validate-response.tool.d.ts +13 -0
  104. package/dist/tools/validate-response/validate-response.tool.d.ts.map +1 -0
  105. package/dist/tools/validate-response/validate-response.tool.js +142 -0
  106. package/dist/tools/validate-response/validate-response.tool.js.map +1 -0
  107. package/dist/types/request-context.d.ts +7 -0
  108. package/dist/types/request-context.d.ts.map +1 -0
  109. package/dist/types/request-context.js +7 -0
  110. package/dist/types/request-context.js.map +1 -0
  111. 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