@magic-ingredients/tiny-brain-local 0.8.0 → 0.10.2

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 (147) hide show
  1. package/dist/agents/formatters/claude-code-formatter.d.ts +37 -0
  2. package/dist/agents/formatters/claude-code-formatter.d.ts.map +1 -0
  3. package/dist/agents/formatters/claude-code-formatter.js +217 -0
  4. package/dist/agents/formatters/claude-code-formatter.js.map +1 -0
  5. package/dist/agents/formatters/formatter-factory.d.ts +25 -0
  6. package/dist/agents/formatters/formatter-factory.d.ts.map +1 -0
  7. package/dist/agents/formatters/formatter-factory.js +61 -0
  8. package/dist/agents/formatters/formatter-factory.js.map +1 -0
  9. package/dist/agents/types.d.ts +68 -0
  10. package/dist/agents/types.d.ts.map +1 -0
  11. package/dist/agents/types.js +12 -0
  12. package/dist/agents/types.js.map +1 -0
  13. package/dist/analyser/analyzers/script-analyzer.d.ts +10 -0
  14. package/dist/analyser/analyzers/script-analyzer.d.ts.map +1 -0
  15. package/dist/analyser/analyzers/script-analyzer.js +205 -0
  16. package/dist/analyser/analyzers/script-analyzer.js.map +1 -0
  17. package/dist/analyser/detectors/base-detector.d.ts +12 -0
  18. package/dist/analyser/detectors/base-detector.d.ts.map +1 -0
  19. package/dist/analyser/detectors/base-detector.js +50 -0
  20. package/dist/analyser/detectors/base-detector.js.map +1 -0
  21. package/dist/analyser/detectors/javascript-detector.d.ts +19 -0
  22. package/dist/analyser/detectors/javascript-detector.d.ts.map +1 -0
  23. package/dist/analyser/detectors/javascript-detector.js +347 -0
  24. package/dist/analyser/detectors/javascript-detector.js.map +1 -0
  25. package/dist/analyser/index.d.ts +5 -0
  26. package/dist/analyser/index.d.ts.map +1 -0
  27. package/dist/analyser/index.js +315 -0
  28. package/dist/analyser/index.js.map +1 -0
  29. package/dist/analyser/types.d.ts +2 -0
  30. package/dist/analyser/types.d.ts.map +1 -0
  31. package/dist/analyser/types.js +2 -0
  32. package/dist/analyser/types.js.map +1 -0
  33. package/dist/analyser/utils.d.ts +5 -0
  34. package/dist/analyser/utils.d.ts.map +1 -0
  35. package/dist/analyser/utils.js +24 -0
  36. package/dist/analyser/utils.js.map +1 -0
  37. package/dist/cli/cli-factory.d.ts.map +1 -1
  38. package/dist/cli/cli-factory.js +17 -0
  39. package/dist/cli/cli-factory.js.map +1 -1
  40. package/dist/cli/commands/analyse.command.d.ts +7 -0
  41. package/dist/cli/commands/analyse.command.d.ts.map +1 -0
  42. package/dist/cli/commands/analyse.command.js +130 -0
  43. package/dist/cli/commands/analyse.command.js.map +1 -0
  44. package/dist/cli/commands/status.command.d.ts.map +1 -1
  45. package/dist/cli/commands/status.command.js +3 -1
  46. package/dist/cli/commands/status.command.js.map +1 -1
  47. package/dist/core/mcp-server.d.ts +10 -8
  48. package/dist/core/mcp-server.d.ts.map +1 -1
  49. package/dist/core/mcp-server.js +93 -85
  50. package/dist/core/mcp-server.js.map +1 -1
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +33 -8
  54. package/dist/index.js.map +1 -1
  55. package/dist/prompts/persona/persona.prompt.js +8 -8
  56. package/dist/prompts/persona/persona.prompt.js.map +1 -1
  57. package/dist/prompts/planning/planning.prompt.d.ts +0 -8
  58. package/dist/prompts/planning/planning.prompt.d.ts.map +1 -1
  59. package/dist/prompts/planning/planning.prompt.js +0 -175
  60. package/dist/prompts/planning/planning.prompt.js.map +1 -1
  61. package/dist/services/agent-installation-service.d.ts +101 -0
  62. package/dist/services/agent-installation-service.d.ts.map +1 -0
  63. package/dist/services/agent-installation-service.js +328 -0
  64. package/dist/services/agent-installation-service.js.map +1 -0
  65. package/dist/services/agent-manager.d.ts +45 -0
  66. package/dist/services/agent-manager.d.ts.map +1 -0
  67. package/dist/services/agent-manager.js +154 -0
  68. package/dist/services/agent-manager.js.map +1 -0
  69. package/dist/services/agent-service.d.ts +70 -0
  70. package/dist/services/agent-service.d.ts.map +1 -0
  71. package/dist/services/agent-service.js +273 -0
  72. package/dist/services/agent-service.js.map +1 -0
  73. package/dist/services/analyse-service.d.ts +97 -0
  74. package/dist/services/analyse-service.d.ts.map +1 -0
  75. package/dist/services/analyse-service.js +370 -0
  76. package/dist/services/analyse-service.js.map +1 -0
  77. package/dist/services/dashboard-launcher.service.d.ts +20 -0
  78. package/dist/services/dashboard-launcher.service.d.ts.map +1 -0
  79. package/dist/services/dashboard-launcher.service.js +30 -0
  80. package/dist/services/dashboard-launcher.service.js.map +1 -0
  81. package/dist/services/persona-enhancer.d.ts +52 -0
  82. package/dist/services/persona-enhancer.d.ts.map +1 -0
  83. package/dist/services/persona-enhancer.js +252 -0
  84. package/dist/services/persona-enhancer.js.map +1 -0
  85. package/dist/services/persona-grouper.d.ts +29 -0
  86. package/dist/services/persona-grouper.d.ts.map +1 -0
  87. package/dist/services/persona-grouper.js +111 -0
  88. package/dist/services/persona-grouper.js.map +1 -0
  89. package/dist/services/persona-service.d.ts +52 -0
  90. package/dist/services/persona-service.d.ts.map +1 -0
  91. package/dist/services/{enhanced-persona-service.js → persona-service.js} +125 -7
  92. package/dist/services/persona-service.js.map +1 -0
  93. package/dist/services/remote/auth-token-service.d.ts.map +1 -1
  94. package/dist/services/remote/auth-token-service.js +10 -3
  95. package/dist/services/remote/auth-token-service.js.map +1 -1
  96. package/dist/services/remote/system-persona-service.d.ts.map +1 -1
  97. package/dist/services/remote/system-persona-service.js +41 -10
  98. package/dist/services/remote/system-persona-service.js.map +1 -1
  99. package/dist/services/repo-service.d.ts +195 -0
  100. package/dist/services/repo-service.d.ts.map +1 -0
  101. package/dist/services/repo-service.js +1023 -0
  102. package/dist/services/repo-service.js.map +1 -0
  103. package/dist/services/types/persona-types.d.ts +84 -0
  104. package/dist/services/types/persona-types.d.ts.map +1 -0
  105. package/dist/services/types/persona-types.js +5 -0
  106. package/dist/services/types/persona-types.js.map +1 -0
  107. package/dist/services/versioning-service.d.ts +79 -0
  108. package/dist/services/versioning-service.d.ts.map +1 -0
  109. package/dist/services/versioning-service.js +191 -0
  110. package/dist/services/versioning-service.js.map +1 -0
  111. package/dist/storage/local-filesystem-adapter.d.ts +1 -0
  112. package/dist/storage/local-filesystem-adapter.d.ts.map +1 -1
  113. package/dist/storage/local-filesystem-adapter.js +47 -3
  114. package/dist/storage/local-filesystem-adapter.js.map +1 -1
  115. package/dist/storage/platform-config-adapter.d.ts +9 -0
  116. package/dist/storage/platform-config-adapter.d.ts.map +1 -1
  117. package/dist/storage/platform-config-adapter.js +55 -1
  118. package/dist/storage/platform-config-adapter.js.map +1 -1
  119. package/dist/tools/analyse.tool.d.ts +17 -0
  120. package/dist/tools/analyse.tool.d.ts.map +1 -0
  121. package/dist/tools/analyse.tool.js +124 -0
  122. package/dist/tools/analyse.tool.js.map +1 -0
  123. package/dist/tools/persona/as.tool.d.ts +32 -11
  124. package/dist/tools/persona/as.tool.d.ts.map +1 -1
  125. package/dist/tools/persona/as.tool.js +452 -317
  126. package/dist/tools/persona/as.tool.js.map +1 -1
  127. package/dist/tools/persona/persona.tool.js +2 -2
  128. package/dist/tools/persona/persona.tool.js.map +1 -1
  129. package/dist/tools/plan/plan.tool.d.ts +3 -3
  130. package/dist/tools/plan/plan.tool.d.ts.map +1 -1
  131. package/dist/tools/plan/plan.tool.js +78 -55
  132. package/dist/tools/plan/plan.tool.js.map +1 -1
  133. package/dist/tools/tool-registry.d.ts.map +1 -1
  134. package/dist/tools/tool-registry.js +4 -0
  135. package/dist/tools/tool-registry.js.map +1 -1
  136. package/dist/utils/repo-utils.d.ts +10 -0
  137. package/dist/utils/repo-utils.d.ts.map +1 -0
  138. package/dist/utils/repo-utils.js +55 -0
  139. package/dist/utils/repo-utils.js.map +1 -0
  140. package/package.json +6 -2
  141. package/dist/services/enhanced-persona-service.d.ts +0 -22
  142. package/dist/services/enhanced-persona-service.d.ts.map +0 -1
  143. package/dist/services/enhanced-persona-service.js.map +0 -1
  144. package/dist/services/plan-watcher.service.d.ts +0 -141
  145. package/dist/services/plan-watcher.service.d.ts.map +0 -1
  146. package/dist/services/plan-watcher.service.js +0 -1010
  147. package/dist/services/plan-watcher.service.js.map +0 -1
@@ -1,1010 +0,0 @@
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 { PlanningService } from '@magic-ingredients/tiny-brain-core';
13
- /**
14
- * Singleton instance tracking
15
- */
16
- let runningInstance = null;
17
- /**
18
- * Plan Watcher Service
19
- */
20
- export class PlanWatcherService {
21
- server = null;
22
- watchInterval = null;
23
- port = 0;
24
- eventEmitter = new EventEmitter();
25
- context;
26
- logger;
27
- activeConnections = new Set();
28
- boundOnPersonaChanged = null;
29
- // File timestamp tracking for change detection (only tracks timestamps, not content)
30
- fileTimestamps = new Map();
31
- // Track last known persona for change detection
32
- lastKnownPersona = null;
33
- constructor(context) {
34
- this.context = context;
35
- this.logger = context.logger;
36
- this.logger.info('PlanWatcherService initialized (stateless mode)');
37
- }
38
- /**
39
- * Start the plan watcher dashboard
40
- */
41
- async start(options = {}) {
42
- try {
43
- // Check if already running
44
- if (this.isRunning()) {
45
- return {
46
- success: false,
47
- error: new Error('Plan watcher is already running on port ' + this.port)
48
- };
49
- }
50
- // Check singleton
51
- if (runningInstance && runningInstance !== this) {
52
- return {
53
- success: false,
54
- error: new Error('Another plan watcher instance is already running')
55
- };
56
- }
57
- // Register persona change listener to detect changes
58
- if (this.context.personaChangeListeners) {
59
- this.boundOnPersonaChanged = this.onPersonaChanged.bind(this);
60
- this.context.personaChangeListeners.push(this.boundOnPersonaChanged);
61
- this.logger.info('Registered persona change listener');
62
- }
63
- // Set options
64
- this.port = options.port || 8765;
65
- const autoOpen = options.autoOpen !== undefined
66
- ? options.autoOpen
67
- : true; // Default to true
68
- // No need to load plans initially - they'll be loaded on demand
69
- // Create HTTP server
70
- this.server = this.createHttpServer();
71
- // Start server
72
- await new Promise((resolve, reject) => {
73
- const serverInstance = this.server;
74
- if (!serverInstance)
75
- throw new Error('Server not created');
76
- serverInstance.on('error', (err) => {
77
- if (err.code === 'EADDRINUSE') {
78
- reject(new Error(`Port ${this.port} is already in use`));
79
- }
80
- else {
81
- reject(err);
82
- }
83
- });
84
- serverInstance.listen(this.port, () => {
85
- this.logger.info(`Plan watcher started on port ${this.port}`);
86
- resolve();
87
- });
88
- });
89
- // Set singleton instance
90
- // eslint-disable-next-line @typescript-eslint/no-this-alias
91
- runningInstance = this;
92
- // Start watching for changes
93
- await this.startWatching();
94
- const url = `http://localhost:${this.port}`;
95
- // Open browser if autoOpen is enabled
96
- if (autoOpen) {
97
- // Browser opening functionality removed - user can manually open the URL
98
- this.logger.info(`Dashboard available at ${url} - please open manually`);
99
- }
100
- return {
101
- success: true,
102
- data: {
103
- url,
104
- port: this.port,
105
- autoOpen,
106
- plansLoaded: 0 // Plans are loaded on demand now
107
- }
108
- };
109
- }
110
- catch (error) {
111
- return {
112
- success: false,
113
- error: new Error(`Failed to start plan watcher: ${error instanceof Error ? error.message : 'Unknown error'}`)
114
- };
115
- }
116
- }
117
- /**
118
- * Stop the plan watcher dashboard
119
- */
120
- async stop() {
121
- try {
122
- if (!this.isRunning()) {
123
- return {
124
- success: false,
125
- error: new Error('Plan watcher is not running')
126
- };
127
- }
128
- // Unregister our callback
129
- if (this.context.personaChangeListeners && this.boundOnPersonaChanged) {
130
- const index = this.context.personaChangeListeners.indexOf(this.boundOnPersonaChanged);
131
- if (index > -1) {
132
- this.context.personaChangeListeners.splice(index, 1);
133
- }
134
- this.boundOnPersonaChanged = null;
135
- }
136
- // Stop polling interval
137
- if (this.watchInterval) {
138
- clearInterval(this.watchInterval);
139
- this.watchInterval = null;
140
- }
141
- // Close all active SSE connections first
142
- // This is important to prevent hanging on server.close()
143
- for (const connection of this.activeConnections) {
144
- try {
145
- connection.end();
146
- }
147
- catch {
148
- // Connection might already be closed
149
- }
150
- }
151
- this.activeConnections.clear();
152
- // Stop HTTP server
153
- if (this.server) {
154
- const serverToClose = this.server;
155
- this.server = null; // Clear reference immediately
156
- // Use a timeout to prevent indefinite hanging
157
- await Promise.race([
158
- new Promise((resolve) => {
159
- serverToClose.close(() => {
160
- this.logger.info('Plan watcher stopped');
161
- resolve();
162
- });
163
- }),
164
- new Promise((resolve) => {
165
- setTimeout(() => {
166
- this.logger.warn('Force closing server after timeout');
167
- resolve();
168
- }, 1000); // 1 second timeout
169
- })
170
- ]);
171
- }
172
- // Clear singleton if it's this instance
173
- if (runningInstance === this) {
174
- runningInstance = null;
175
- }
176
- // Clear tracking data
177
- this.fileTimestamps.clear();
178
- this.lastKnownPersona = null;
179
- this.port = 0;
180
- return { success: true, data: undefined };
181
- }
182
- catch (error) {
183
- return {
184
- success: false,
185
- error: new Error(`Failed to stop plan watcher: ${error instanceof Error ? error.message : 'Unknown error'}`)
186
- };
187
- }
188
- }
189
- /**
190
- * Check if the watcher is running
191
- */
192
- isRunning() {
193
- return this.server !== null && this.port > 0;
194
- }
195
- /**
196
- * Get the URL of the running watcher
197
- */
198
- getUrl() {
199
- if (!this.isRunning()) {
200
- return {
201
- success: false,
202
- error: new Error('Plan watcher is not running')
203
- };
204
- }
205
- return {
206
- success: true,
207
- data: `http://localhost:${this.port}`
208
- };
209
- }
210
- /**
211
- * Get the current active persona ID (stateless)
212
- * Uses the callback to always get the latest persona from MCP server
213
- */
214
- getCurrentPersonaId() {
215
- this.logger.debug('getCurrentPersonaId called');
216
- // Use callback if available
217
- if (this.context.getCurrentActivePersona) {
218
- const personaId = this.context.getCurrentActivePersona();
219
- this.logger.debug(`Callback returned: ${personaId}`);
220
- if (personaId) {
221
- return personaId;
222
- }
223
- }
224
- else {
225
- this.logger.debug('No getCurrentActivePersona callback available');
226
- }
227
- // Fallback to activePersona from context
228
- if (this.context.activePersona?.id) {
229
- this.logger.debug(`Using context.activePersona.id: ${this.context.activePersona.id}`);
230
- return this.context.activePersona.id;
231
- }
232
- // Default if no persona available
233
- this.logger.debug('Using default persona');
234
- return 'default';
235
- }
236
- /**
237
- * Handle persona change notification from MCP server
238
- * This is called when 'as' tool or 'become' prompt changes the persona
239
- */
240
- async onPersonaChanged(newPersonaId) {
241
- this.logger.info(`CALLBACK TRIGGERED: Persona changed to ${newPersonaId}`);
242
- // CRITICAL: Update the context's active persona so PlanningService loads the right plans
243
- if (this.context.activePersona) {
244
- this.context.activePersona.id = newPersonaId;
245
- }
246
- else {
247
- // Create a minimal persona object if it doesn't exist
248
- this.context.activePersona = {
249
- id: newPersonaId,
250
- userContent: {}
251
- };
252
- }
253
- // Clear file timestamps since we're switching personas
254
- this.fileTimestamps.clear();
255
- // Emit SSE event to notify frontend of persona change
256
- this.logger.info(`Emitting persona-changed event for: ${newPersonaId}`);
257
- this.eventEmitter.emit('update', {
258
- type: 'persona-changed',
259
- personaId: newPersonaId,
260
- changeType: 'persona',
261
- message: `Persona changed to ${newPersonaId}`
262
- });
263
- }
264
- /**
265
- * Load plans from storage using PlanningService (stateless)
266
- * Returns plans directly without storing them
267
- */
268
- async loadPlans(type) {
269
- try {
270
- // The context should already have the active persona from the MCP layer
271
- const planningService = new PlanningService(this.context);
272
- // Load plans based on type
273
- const plans = type ?
274
- await planningService.listPlans({ type }) :
275
- await planningService.listPlans(); // Get all plans
276
- // Get the currently active plan ID
277
- const activePlan = await planningService.getActivePlan();
278
- const activePlanId = activePlan?.id || null;
279
- this.logger.info(`[loadPlans] Loaded ${plans.length} plans, active: ${activePlanId || 'none'}`);
280
- return { plans, activePlanId };
281
- }
282
- catch (error) {
283
- this.logger.error('Failed to load plans:', error);
284
- return { plans: [], activePlanId: null };
285
- }
286
- }
287
- /**
288
- * Start watching for plan file changes
289
- */
290
- async startWatching() {
291
- // Set up polling interval to check for changes
292
- const POLL_INTERVAL = 2000; // Check every 2 seconds
293
- this.watchInterval = setInterval(async () => {
294
- await this.checkForChanges();
295
- }, POLL_INTERVAL);
296
- // Initial snapshot
297
- await this.updateFileSnapshot();
298
- }
299
- /**
300
- * Check for changes in plan files
301
- */
302
- async checkForChanges() {
303
- try {
304
- // Skip checking if no active persona yet
305
- if (!this.context.activePersona?.id) {
306
- this.logger.debug('No active persona, skipping plan check');
307
- return;
308
- }
309
- // Get current persona to check for changes
310
- const currentPersona = this.getCurrentPersonaId();
311
- // Check if persona changed
312
- if (this.lastKnownPersona && this.lastKnownPersona !== currentPersona) {
313
- this.logger.info(`Persona changed from ${this.lastKnownPersona} to ${currentPersona}`);
314
- this.lastKnownPersona = currentPersona;
315
- // Clear timestamps as we're in a new persona context
316
- this.fileTimestamps.clear();
317
- // Load all plans for the new persona
318
- const { plans, activePlanId } = await this.loadPlans();
319
- // Emit persona change event with full plan data
320
- this.eventEmitter.emit('update', {
321
- type: 'personaChange',
322
- personaId: currentPersona,
323
- plans: plans, // Full plan data
324
- activePlanId: activePlanId,
325
- timestamp: new Date().toISOString()
326
- });
327
- return;
328
- }
329
- // Update last known persona if not set
330
- if (!this.lastKnownPersona) {
331
- this.lastKnownPersona = currentPersona;
332
- }
333
- // Load current plans to check for changes
334
- // The context should already have the active persona from the MCP layer
335
- const planningService = new PlanningService(this.context);
336
- const currentPlans = await planningService.listPlans();
337
- // Build current state map for comparison
338
- const currentTimestamps = new Map();
339
- for (const plan of currentPlans) {
340
- // Use lastUpdated timestamp or a hash of the plan content
341
- const timestamp = plan.lastUpdated ?
342
- new Date(plan.lastUpdated).getTime() :
343
- JSON.stringify(plan).length; // Simple content hash
344
- currentTimestamps.set(plan.id, timestamp);
345
- }
346
- // Detect changes by comparing timestamps and track specific changes
347
- let hasChanges = false;
348
- const changedPlanIds = [];
349
- const removedPlanIds = [];
350
- // Check if the number of plans changed
351
- if (this.fileTimestamps.size !== currentTimestamps.size) {
352
- hasChanges = true;
353
- this.logger.info(`Plan count changed: ${this.fileTimestamps.size} -> ${currentTimestamps.size}`);
354
- }
355
- // Check for removed plans
356
- for (const [id] of this.fileTimestamps) {
357
- if (!currentTimestamps.has(id)) {
358
- hasChanges = true;
359
- removedPlanIds.push(id);
360
- this.logger.info(`Plan removed: ${id}`);
361
- }
362
- }
363
- // Check for added or modified plans
364
- for (const [id, timestamp] of currentTimestamps) {
365
- const previousTimestamp = this.fileTimestamps.get(id);
366
- if (!previousTimestamp) {
367
- hasChanges = true;
368
- changedPlanIds.push(id);
369
- this.logger.info(`Plan added: ${id}`);
370
- }
371
- else if (previousTimestamp !== timestamp) {
372
- hasChanges = true;
373
- changedPlanIds.push(id);
374
- this.logger.info(`Plan modified: ${id}`);
375
- }
376
- }
377
- // Update our timestamp tracking
378
- this.fileTimestamps = currentTimestamps;
379
- // Emit SSE event if changes detected
380
- if (hasChanges) {
381
- this.logger.info(`Changes detected - loading ${changedPlanIds.length} changed plans, ${removedPlanIds.length} removed`);
382
- // Load only the changed plans (new or modified)
383
- const changedPlans = [];
384
- for (const planId of changedPlanIds) {
385
- const plan = currentPlans.find(p => p.id === planId);
386
- if (plan) {
387
- changedPlans.push(plan);
388
- }
389
- }
390
- // Get the active plan ID
391
- const planningService = new PlanningService(this.context);
392
- const activePlan = await planningService.getActivePlan();
393
- const activePlanId = activePlan?.id || null;
394
- const eventData = {
395
- type: 'planChange',
396
- changedPlans: changedPlans, // Only the plans that changed (with full content)
397
- removedPlanIds: removedPlanIds, // IDs of deleted plans
398
- activePlanId: activePlanId,
399
- personaId: this.getCurrentPersonaId(),
400
- changeType: 'incremental', // Indicates this is a partial update
401
- timestamp: new Date().toISOString(),
402
- version: 'v3-incremental' // New version with incremental updates
403
- };
404
- this.logger.info(`Emitting planChange event: ${changedPlans.length} changed, ${removedPlanIds.length} removed`);
405
- this.eventEmitter.emit('update', eventData);
406
- }
407
- else {
408
- this.logger.debug('No changes detected');
409
- }
410
- }
411
- catch (error) {
412
- this.logger.error('Error checking for plan changes:', error);
413
- }
414
- }
415
- /**
416
- * Update the file snapshot for change detection
417
- */
418
- async updateFileSnapshot() {
419
- try {
420
- // Load current plans to get initial timestamps
421
- // The context should already have the active persona from the MCP layer
422
- const planningService = new PlanningService(this.context);
423
- const currentPlans = await planningService.listPlans();
424
- // Build initial timestamp map
425
- this.fileTimestamps.clear();
426
- for (const plan of currentPlans) {
427
- const timestamp = plan.lastUpdated ?
428
- new Date(plan.lastUpdated).getTime() :
429
- JSON.stringify(plan).length;
430
- this.fileTimestamps.set(plan.id, timestamp);
431
- }
432
- // Store initial persona
433
- this.lastKnownPersona = this.getCurrentPersonaId();
434
- this.logger.info(`Initial snapshot: ${currentPlans.length} plans, persona: ${this.lastKnownPersona}`);
435
- }
436
- catch (error) {
437
- this.logger.error('Failed to update file snapshot:', error);
438
- }
439
- }
440
- /**
441
- * Create the HTTP server for the dashboard
442
- */
443
- createHttpServer() {
444
- return http.createServer((req, res) => {
445
- const url = new globalThis.URL(req.url || '/', `http://localhost:${this.port}`);
446
- // Add CORS headers for all requests
447
- res.setHeader('Access-Control-Allow-Origin', '*');
448
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
449
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept');
450
- // Handle preflight requests
451
- if (req.method === 'OPTIONS') {
452
- res.writeHead(200);
453
- res.end();
454
- return;
455
- }
456
- // Route handling
457
- if (url.pathname === '/') {
458
- this.serveDashboard(res);
459
- }
460
- else if (url.pathname === '/api/plans') {
461
- // Support both old 'archived' param and new 'type' param
462
- const typeParam = url.searchParams.get('type');
463
- const archivedParam = url.searchParams.get('archived');
464
- let planType;
465
- if (typeParam === 'active' || typeParam === 'archived') {
466
- planType = typeParam;
467
- }
468
- else if (archivedParam === 'true') {
469
- planType = 'archived'; // Backward compatibility
470
- }
471
- else if (archivedParam === 'false') {
472
- planType = 'active'; // Backward compatibility
473
- }
474
- // If no param or invalid param, planType remains undefined (returns all)
475
- this.servePlansApi(res, planType);
476
- }
477
- else if (url.pathname.startsWith('/api/plan/')) {
478
- const pathParts = url.pathname.substring('/api/plan/'.length).split('/');
479
- const planId = pathParts[0];
480
- const action = pathParts[1];
481
- // Handle plan management actions
482
- if (req.method === 'POST' && action) {
483
- this.handlePlanAction(req, res, planId, action);
484
- }
485
- else if (req.method === 'DELETE' && !action) {
486
- this.handlePlanDelete(req, res, planId);
487
- }
488
- else {
489
- // Default GET request for plan details
490
- this.servePlanApi(planId, url, res);
491
- }
492
- }
493
- else if (url.pathname === '/api/persona') {
494
- this.servePersonaApi(req, res);
495
- }
496
- else if (url.pathname === '/events') {
497
- this.serveEventStream(req, res);
498
- }
499
- else if (url.pathname.startsWith('/assets/') || url.pathname.endsWith('.js') || url.pathname.endsWith('.css')) {
500
- // Serve static assets for React dashboard
501
- this.serveStaticAsset(url.pathname, res);
502
- }
503
- else {
504
- // For client-side routing, serve index.html for non-API routes
505
- const dashboardPath = this.getReactDashboardPath();
506
- if (dashboardPath && fs.existsSync(dashboardPath)) {
507
- this.serveReactDashboard(res);
508
- }
509
- else {
510
- res.writeHead(404);
511
- res.end('Not Found');
512
- }
513
- }
514
- });
515
- }
516
- /**
517
- * Serve the main dashboard HTML
518
- */
519
- serveDashboard(res) {
520
- // Check if React build exists
521
- const dashboardPath = this.getReactDashboardPath();
522
- if (dashboardPath && fs.existsSync(dashboardPath)) {
523
- this.serveReactDashboard(res);
524
- }
525
- else {
526
- // Fallback to legacy HTML dashboard
527
- const html = this.loadDashboardHtml();
528
- res.writeHead(200, { 'Content-Type': 'text/html' });
529
- res.end(html);
530
- }
531
- }
532
- /**
533
- * Serve the plans list API (stateless - fetches fresh data)
534
- */
535
- async servePlansApi(res, planType) {
536
- try {
537
- // Load plans based on type (undefined means all)
538
- // The context should already have the active persona from the MCP layer
539
- const planningService = new PlanningService(this.context);
540
- this.logger.debug(`[servePlansApi] Requesting plans with type: ${planType}`);
541
- const plans = await planningService.listPlans(planType ? { type: planType } : {});
542
- this.logger.debug(`[servePlansApi] Retrieved ${plans.length} plans`);
543
- // Get the currently active plan ID
544
- const activePlan = await planningService.getActivePlan();
545
- const activePlanId = activePlan?.id || null;
546
- // Get current persona
547
- const currentPersona = this.getCurrentPersonaId();
548
- // Transform plans to API format with full data
549
- // The plan object from PlanningService might be typed as Plan which limits fields
550
- // We need to treat it as a raw object to preserve ALL fields from the JSON file
551
- const plansList = plans.map((plan) => {
552
- const isTheCurrent = plan.id === activePlanId;
553
- // Cast plan to any to access all fields from the JSON
554
- const planAsAny = plan;
555
- // Return the FULL plan object with computed fields
556
- return {
557
- // Spread ALL fields from the original plan JSON
558
- ...planAsAny,
559
- // Add our computed/additional fields
560
- isTheCurrentPlan: isTheCurrent,
561
- name: planAsAny.title || 'Unnamed Plan',
562
- phaseCount: planAsAny.phases?.length || 0,
563
- progress: this.calculateProgress(planAsAny)
564
- };
565
- });
566
- // Log the first plan to see what we're actually sending
567
- if (plansList.length > 0) {
568
- const firstPlan = plansList[0];
569
- this.logger.debug(`First plan has phases: ${!!firstPlan.phases}`);
570
- this.logger.debug(`First plan phases length: ${firstPlan.phases?.length}`);
571
- this.logger.debug(`First plan keys before JSON.stringify: ${Object.keys(firstPlan).join(', ')}`);
572
- // Test JSON serialization
573
- const testJson = JSON.stringify(firstPlan);
574
- const parsed = JSON.parse(testJson);
575
- this.logger.debug(`First plan keys after JSON round-trip: ${Object.keys(parsed).join(', ')}`);
576
- }
577
- const response = {
578
- plans: plansList,
579
- persona: currentPersona,
580
- activePlanId: activePlanId
581
- };
582
- res.writeHead(200, { 'Content-Type': 'application/json' });
583
- res.end(JSON.stringify(response));
584
- }
585
- catch (error) {
586
- this.logger.error('Error serving plans API:', error);
587
- res.writeHead(500, { 'Content-Type': 'application/json' });
588
- res.end(JSON.stringify({ error: 'Failed to load plans' }));
589
- }
590
- }
591
- /**
592
- * Serve a specific plan API
593
- */
594
- async servePlanApi(planId, url, res) {
595
- // ALWAYS load from storage - it's local and fast!
596
- // The context should already have the active persona from the MCP layer
597
- const planningService = new PlanningService(this.context);
598
- let plan;
599
- try {
600
- plan = await planningService.loadPlan({ planId });
601
- }
602
- catch (error) {
603
- this.logger.error(`[servePlanApi] Error loading plan ${planId}:`, error);
604
- }
605
- if (!plan) {
606
- res.writeHead(404, { 'Content-Type': 'application/json' });
607
- res.end(JSON.stringify({ error: 'Plan not found' }));
608
- return;
609
- }
610
- // Check if we want the formatted report
611
- const format = url.searchParams.get('format');
612
- if (format === 'report') {
613
- try {
614
- // Use PlanningService to generate the status report
615
- // Context already has activePersona from above
616
- const planningService = new PlanningService(this.context);
617
- const report = await planningService.generateStatusReport(plan);
618
- res.writeHead(200, { 'Content-Type': 'application/json' });
619
- res.end(JSON.stringify({
620
- report,
621
- format: 'markdown'
622
- }));
623
- }
624
- catch (error) {
625
- this.logger.error('Failed to generate plan report:', error);
626
- res.writeHead(500, { 'Content-Type': 'application/json' });
627
- res.end(JSON.stringify({ error: 'Failed to generate report' }));
628
- }
629
- }
630
- else {
631
- // Return raw JSON as before
632
- res.writeHead(200, { 'Content-Type': 'application/json' });
633
- res.end(JSON.stringify(plan));
634
- }
635
- }
636
- /**
637
- * Serve the persona API
638
- * GET /api/persona - Get current persona and their plans
639
- */
640
- async servePersonaApi(req, res) {
641
- if (req.method === 'GET') {
642
- // Get current persona
643
- let personaId;
644
- if (this.context.getCurrentActivePersona) {
645
- personaId = this.context.getCurrentActivePersona() || 'default';
646
- this.logger.info(`/api/persona - callback returned: ${personaId}`);
647
- }
648
- else {
649
- // Fallback if no callback available
650
- personaId = this.getCurrentPersonaId();
651
- this.logger.warn(`/api/persona - no callback, using fallback: ${personaId}`);
652
- }
653
- // Also load the persona's plans for convenience
654
- const { plans, activePlanId } = await this.loadPlans();
655
- res.writeHead(200, { 'Content-Type': 'application/json' });
656
- res.end(JSON.stringify({
657
- personaId: personaId,
658
- plans: plans,
659
- activePlanId: activePlanId
660
- }));
661
- }
662
- else {
663
- // Persona changes are handled by MCP server via 'as' tool
664
- res.writeHead(405, { 'Content-Type': 'application/json' });
665
- res.end(JSON.stringify({
666
- error: 'Method not allowed. Use the "as" MCP tool to change personas.'
667
- }));
668
- }
669
- }
670
- /**
671
- * Handle plan management actions (archive, unarchive, activate)
672
- */
673
- async handlePlanAction(_req, res, planId, action) {
674
- try {
675
- // The context should already have the active persona from the MCP layer
676
- if (!this.context.activePersona) {
677
- res.writeHead(400, { 'Content-Type': 'application/json' });
678
- res.end(JSON.stringify({ error: 'No active persona' }));
679
- return;
680
- }
681
- const planningService = new PlanningService(this.context);
682
- let success = false;
683
- let errorMessage = '';
684
- let newActivePlanId = null;
685
- try {
686
- switch (action) {
687
- case 'archive':
688
- await planningService.archivePlan({ planId, reason: 'Archived via dashboard' });
689
- success = true;
690
- break;
691
- case 'unarchive':
692
- await planningService.unarchivePlan(planId);
693
- success = true;
694
- break;
695
- case 'activate':
696
- this.logger.info(`Setting active plan to: ${planId}`);
697
- await planningService.switchToPlan(planId);
698
- newActivePlanId = planId;
699
- success = true;
700
- this.logger.info(`Active plan is now: ${planId}`);
701
- break;
702
- default:
703
- res.writeHead(400, { 'Content-Type': 'application/json' });
704
- res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
705
- return;
706
- }
707
- }
708
- catch (error) {
709
- success = false;
710
- errorMessage = error instanceof Error ? error.message : 'Operation failed';
711
- this.logger.error(`Error in plan action ${action} for ${planId}: ${errorMessage}`, error);
712
- }
713
- if (success) {
714
- // Return success response
715
- const response = {
716
- success: true,
717
- message: `Plan ${action}d successfully`
718
- };
719
- if (newActivePlanId) {
720
- response.activePlanId = newActivePlanId;
721
- }
722
- res.writeHead(200, { 'Content-Type': 'application/json' });
723
- res.end(JSON.stringify(response));
724
- // Emit planChange event for real-time sync with full data
725
- const { plans, activePlanId } = await this.loadPlans();
726
- this.eventEmitter.emit('update', {
727
- type: 'planChange',
728
- plans: plans,
729
- activePlanId: activePlanId,
730
- personaId: this.getCurrentPersonaId(),
731
- action,
732
- planId,
733
- timestamp: new Date().toISOString()
734
- });
735
- }
736
- else {
737
- res.writeHead(400, { 'Content-Type': 'application/json' });
738
- res.end(JSON.stringify({ error: errorMessage || `Failed to ${action} plan` }));
739
- }
740
- }
741
- catch (error) {
742
- this.logger.error(`Error handling plan action ${action} for ${planId}:`, error);
743
- res.writeHead(500, { 'Content-Type': 'application/json' });
744
- res.end(JSON.stringify({ error: 'Internal server error' }));
745
- }
746
- }
747
- /**
748
- * Handle plan deletion
749
- */
750
- async handlePlanDelete(_req, res, planId) {
751
- try {
752
- // The context should already have the active persona from the MCP layer
753
- if (!this.context.activePersona) {
754
- res.writeHead(400, { 'Content-Type': 'application/json' });
755
- res.end(JSON.stringify({ error: 'No active persona' }));
756
- return;
757
- }
758
- this.context.logger.info(`Attempting to delete plan ${planId} for persona ${this.context.activePersona.id}`);
759
- try {
760
- // Use PlanningService to delete the plan
761
- // It will handle all the checks (archived status, ownership, etc.)
762
- const planningService = new PlanningService(this.context);
763
- const result = await planningService.deletePlan(planId);
764
- if (result) {
765
- this.context.logger.info(`Successfully deleted plan ${planId}`);
766
- // Send success response
767
- res.writeHead(200, { 'Content-Type': 'application/json' });
768
- res.end(JSON.stringify({ success: true, message: 'Plan deleted successfully' }));
769
- // Emit planChange event for real-time sync with full data
770
- const { plans, activePlanId } = await this.loadPlans();
771
- this.eventEmitter.emit('update', {
772
- type: 'planChange',
773
- plans: plans,
774
- activePlanId: activePlanId,
775
- personaId: this.getCurrentPersonaId(),
776
- action: 'delete',
777
- planId,
778
- timestamp: new Date().toISOString()
779
- });
780
- }
781
- else {
782
- // This shouldn't happen as deletePlan throws on error, but handle it just in case
783
- res.writeHead(400, { 'Content-Type': 'application/json' });
784
- res.end(JSON.stringify({ error: 'Failed to delete plan' }));
785
- }
786
- }
787
- catch (error) {
788
- // PlanningService will throw specific errors
789
- const errorMessage = error instanceof Error ? error.message : 'Failed to delete plan';
790
- this.context.logger.error(`Failed to delete plan: ${errorMessage}`);
791
- if (errorMessage.includes('not found')) {
792
- res.writeHead(404, { 'Content-Type': 'application/json' });
793
- res.end(JSON.stringify({ error: 'Plan not found' }));
794
- }
795
- else if (errorMessage.includes('Archive it first')) {
796
- res.writeHead(400, { 'Content-Type': 'application/json' });
797
- res.end(JSON.stringify({ error: 'Only archived plans can be deleted' }));
798
- }
799
- else {
800
- res.writeHead(400, { 'Content-Type': 'application/json' });
801
- res.end(JSON.stringify({ error: errorMessage }));
802
- }
803
- }
804
- }
805
- catch (error) {
806
- this.logger.error(`Error deleting plan ${planId}:`, error);
807
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
808
- // Return more specific error message
809
- if (errorMessage.includes('not found') || errorMessage.includes('does not exist')) {
810
- res.writeHead(404, { 'Content-Type': 'application/json' });
811
- res.end(JSON.stringify({ error: 'Plan not found' }));
812
- }
813
- else {
814
- res.writeHead(500, { 'Content-Type': 'application/json' });
815
- res.end(JSON.stringify({ error: errorMessage }));
816
- }
817
- }
818
- }
819
- /**
820
- * Serve Server-Sent Events for real-time updates
821
- */
822
- async serveEventStream(req, res) {
823
- res.writeHead(200, {
824
- 'Content-Type': 'text/event-stream',
825
- 'Cache-Control': 'no-cache',
826
- 'Connection': 'keep-alive',
827
- 'Access-Control-Allow-Origin': '*',
828
- 'Access-Control-Allow-Methods': 'GET, OPTIONS',
829
- 'Access-Control-Allow-Headers': 'Content-Type'
830
- });
831
- // Track this connection
832
- this.activeConnections.add(res);
833
- // Get current persona and plans for initial sync
834
- const currentPersona = this.getCurrentPersonaId();
835
- this.logger.info(`New SSE connection - sending initial persona: ${currentPersona}`);
836
- // Load initial plans to send with connected event
837
- const { plans, activePlanId } = await this.loadPlans();
838
- // Send initial connection message with current persona AND plans
839
- res.write(`data: ${JSON.stringify({
840
- type: 'connected',
841
- personaId: currentPersona,
842
- plans: plans, // Include all plans in connected event
843
- activePlanId: activePlanId,
844
- timestamp: new Date().toISOString()
845
- })}\n\n`);
846
- // Keep-alive ping every 30 seconds
847
- const keepAlive = setInterval(() => {
848
- res.write(':ping\n\n');
849
- }, 30000);
850
- // Listen for plan updates
851
- const updateHandler = (data) => {
852
- try {
853
- res.write(`data: ${JSON.stringify(data)}\n\n`);
854
- }
855
- catch {
856
- // Connection might be closed
857
- }
858
- };
859
- this.eventEmitter.on('update', updateHandler);
860
- // Clean up on client disconnect
861
- req.on('close', () => {
862
- clearInterval(keepAlive);
863
- this.eventEmitter.off('update', updateHandler);
864
- this.activeConnections.delete(res);
865
- });
866
- }
867
- /**
868
- * Calculate progress percentage for a plan
869
- */
870
- calculateProgress(plan) {
871
- const typedPlan = plan;
872
- if (!typedPlan.phases || typedPlan.phases.length === 0)
873
- return 0;
874
- const completedPhases = typedPlan.phases.filter((p) => p.status === 'completed').length;
875
- return Math.round((completedPhases / typedPlan.phases.length) * 100);
876
- }
877
- /**
878
- * Load and process the dashboard HTML
879
- */
880
- loadDashboardHtml() {
881
- try {
882
- // Get the directory of the current module
883
- const __filename = fileURLToPath(import.meta.url);
884
- const __dirname = path.dirname(__filename);
885
- // Read the dashboard HTML file
886
- const htmlPath = path.join(__dirname, 'dashboard.html');
887
- let html = fs.readFileSync(htmlPath, 'utf-8');
888
- // Replace template variables with current persona
889
- const currentPersona = this.getCurrentPersonaId();
890
- html = html.replace(/{{CURRENT_PERSONA}}/g, currentPersona);
891
- return html;
892
- }
893
- catch (error) {
894
- this.logger.error('Failed to load dashboard HTML:', error);
895
- // Fallback to a simple error page
896
- return this.generateFallbackHtml();
897
- }
898
- }
899
- /**
900
- * Generate fallback HTML if dashboard.html cannot be loaded
901
- */
902
- generateFallbackHtml() {
903
- const currentPersona = this.getCurrentPersonaId();
904
- return `<!DOCTYPE html>
905
- <html>
906
- <head><title>Error</title></head>
907
- <body>
908
- <h1>Dashboard Error</h1>
909
- <p>Failed to load dashboard. Please check the logs.</p>
910
- <p>Current persona: ${currentPersona}</p>
911
- </body>
912
- </html>`;
913
- }
914
- /**
915
- * Get the path to the React dashboard build
916
- */
917
- getReactDashboardPath() {
918
- try {
919
- const __filename = fileURLToPath(import.meta.url);
920
- const __dirname = path.dirname(__filename);
921
- // Try DXT bundle location first (server is at server/index.js, dashboard at dashboard/)
922
- const dxtPath = path.join(__dirname, '..', 'dashboard');
923
- if (fs.existsSync(path.join(dxtPath, 'index.html'))) {
924
- this.logger.info(`Found dashboard at DXT location: ${dxtPath}`);
925
- return dxtPath;
926
- }
927
- // Try production build location
928
- const prodPath = path.join(__dirname, '..', '..', 'dashboard');
929
- if (fs.existsSync(path.join(prodPath, 'index.html'))) {
930
- this.logger.info(`Found dashboard at production location: ${prodPath}`);
931
- return prodPath;
932
- }
933
- // Try development build location
934
- const devPath = path.join(__dirname, '..', '..', '..', 'tiny-brain-dashboard', 'dist');
935
- if (fs.existsSync(path.join(devPath, 'index.html'))) {
936
- this.logger.info(`Found dashboard at development location: ${devPath}`);
937
- return devPath;
938
- }
939
- this.logger.warn('Dashboard not found in any expected location');
940
- return null;
941
- }
942
- catch (error) {
943
- this.logger.error('Failed to locate React dashboard:', error);
944
- return null;
945
- }
946
- }
947
- /**
948
- * Serve the React dashboard
949
- */
950
- serveReactDashboard(res) {
951
- const dashboardPath = this.getReactDashboardPath();
952
- if (!dashboardPath) {
953
- this.serveDashboard(res);
954
- return;
955
- }
956
- const indexPath = path.join(dashboardPath, 'index.html');
957
- const html = fs.readFileSync(indexPath, 'utf-8');
958
- res.writeHead(200, { 'Content-Type': 'text/html' });
959
- res.end(html);
960
- }
961
- /**
962
- * Serve static assets for React dashboard
963
- */
964
- serveStaticAsset(urlPath, res) {
965
- const dashboardPath = this.getReactDashboardPath();
966
- if (!dashboardPath) {
967
- res.writeHead(404);
968
- res.end('Not Found');
969
- return;
970
- }
971
- // Remove leading slash and resolve path
972
- const assetPath = path.join(dashboardPath, urlPath.substring(1));
973
- // Security: Ensure the path is within the dashboard directory
974
- if (!assetPath.startsWith(dashboardPath)) {
975
- res.writeHead(403);
976
- res.end('Forbidden');
977
- return;
978
- }
979
- if (!fs.existsSync(assetPath)) {
980
- res.writeHead(404);
981
- res.end('Not Found');
982
- return;
983
- }
984
- // Determine content type
985
- const ext = path.extname(assetPath).toLowerCase();
986
- const contentTypes = {
987
- '.html': 'text/html',
988
- '.js': 'application/javascript',
989
- '.css': 'text/css',
990
- '.json': 'application/json',
991
- '.png': 'image/png',
992
- '.jpg': 'image/jpeg',
993
- '.gif': 'image/gif',
994
- '.svg': 'image/svg+xml',
995
- '.ico': 'image/x-icon',
996
- };
997
- const contentType = contentTypes[ext] || 'application/octet-stream';
998
- try {
999
- const content = fs.readFileSync(assetPath);
1000
- res.writeHead(200, { 'Content-Type': contentType });
1001
- res.end(content);
1002
- }
1003
- catch (error) {
1004
- this.logger.error('Failed to serve static asset:', error);
1005
- res.writeHead(500);
1006
- res.end('Internal Server Error');
1007
- }
1008
- }
1009
- }
1010
- //# sourceMappingURL=plan-watcher.service.js.map