@magic-ingredients/tiny-brain-local 0.7.0 → 0.10.1

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