@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.
- package/dist/agents/formatters/claude-code-formatter.d.ts +37 -0
- package/dist/agents/formatters/claude-code-formatter.d.ts.map +1 -0
- package/dist/agents/formatters/claude-code-formatter.js +217 -0
- package/dist/agents/formatters/claude-code-formatter.js.map +1 -0
- package/dist/agents/formatters/formatter-factory.d.ts +25 -0
- package/dist/agents/formatters/formatter-factory.d.ts.map +1 -0
- package/dist/agents/formatters/formatter-factory.js +61 -0
- package/dist/agents/formatters/formatter-factory.js.map +1 -0
- package/dist/agents/types.d.ts +68 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +12 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/analyser/analyzers/script-analyzer.d.ts +10 -0
- package/dist/analyser/analyzers/script-analyzer.d.ts.map +1 -0
- package/dist/analyser/analyzers/script-analyzer.js +205 -0
- package/dist/analyser/analyzers/script-analyzer.js.map +1 -0
- package/dist/analyser/detectors/base-detector.d.ts +12 -0
- package/dist/analyser/detectors/base-detector.d.ts.map +1 -0
- package/dist/analyser/detectors/base-detector.js +50 -0
- package/dist/analyser/detectors/base-detector.js.map +1 -0
- package/dist/analyser/detectors/javascript-detector.d.ts +19 -0
- package/dist/analyser/detectors/javascript-detector.d.ts.map +1 -0
- package/dist/analyser/detectors/javascript-detector.js +347 -0
- package/dist/analyser/detectors/javascript-detector.js.map +1 -0
- package/dist/analyser/index.d.ts +5 -0
- package/dist/analyser/index.d.ts.map +1 -0
- package/dist/analyser/index.js +315 -0
- package/dist/analyser/index.js.map +1 -0
- package/dist/analyser/types.d.ts +2 -0
- package/dist/analyser/types.d.ts.map +1 -0
- package/dist/analyser/types.js +2 -0
- package/dist/analyser/types.js.map +1 -0
- package/dist/analyser/utils.d.ts +5 -0
- package/dist/analyser/utils.d.ts.map +1 -0
- package/dist/analyser/utils.js +24 -0
- package/dist/analyser/utils.js.map +1 -0
- package/dist/cli/cli-factory.d.ts.map +1 -1
- package/dist/cli/cli-factory.js +17 -0
- package/dist/cli/cli-factory.js.map +1 -1
- package/dist/cli/commands/analyse.command.d.ts +7 -0
- package/dist/cli/commands/analyse.command.d.ts.map +1 -0
- package/dist/cli/commands/analyse.command.js +130 -0
- package/dist/cli/commands/analyse.command.js.map +1 -0
- package/dist/cli/commands/status.command.d.ts.map +1 -1
- package/dist/cli/commands/status.command.js +3 -1
- package/dist/cli/commands/status.command.js.map +1 -1
- package/dist/core/mcp-server.d.ts +10 -8
- package/dist/core/mcp-server.d.ts.map +1 -1
- package/dist/core/mcp-server.js +93 -85
- package/dist/core/mcp-server.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -8
- package/dist/index.js.map +1 -1
- package/dist/prompts/persona/persona.prompt.js +8 -8
- package/dist/prompts/persona/persona.prompt.js.map +1 -1
- package/dist/prompts/planning/planning.prompt.d.ts +0 -8
- package/dist/prompts/planning/planning.prompt.d.ts.map +1 -1
- package/dist/prompts/planning/planning.prompt.js +0 -175
- package/dist/prompts/planning/planning.prompt.js.map +1 -1
- package/dist/services/agent-installation-service.d.ts +101 -0
- package/dist/services/agent-installation-service.d.ts.map +1 -0
- package/dist/services/agent-installation-service.js +328 -0
- package/dist/services/agent-installation-service.js.map +1 -0
- package/dist/services/agent-manager.d.ts +45 -0
- package/dist/services/agent-manager.d.ts.map +1 -0
- package/dist/services/agent-manager.js +154 -0
- package/dist/services/agent-manager.js.map +1 -0
- package/dist/services/agent-service.d.ts +70 -0
- package/dist/services/agent-service.d.ts.map +1 -0
- package/dist/services/agent-service.js +273 -0
- package/dist/services/agent-service.js.map +1 -0
- package/dist/services/analyse-service.d.ts +97 -0
- package/dist/services/analyse-service.d.ts.map +1 -0
- package/dist/services/analyse-service.js +370 -0
- package/dist/services/analyse-service.js.map +1 -0
- package/dist/services/dashboard-launcher.service.d.ts +20 -0
- package/dist/services/dashboard-launcher.service.d.ts.map +1 -0
- package/dist/services/dashboard-launcher.service.js +30 -0
- package/dist/services/dashboard-launcher.service.js.map +1 -0
- package/dist/services/persona-enhancer.d.ts +52 -0
- package/dist/services/persona-enhancer.d.ts.map +1 -0
- package/dist/services/persona-enhancer.js +252 -0
- package/dist/services/persona-enhancer.js.map +1 -0
- package/dist/services/persona-grouper.d.ts +29 -0
- package/dist/services/persona-grouper.d.ts.map +1 -0
- package/dist/services/persona-grouper.js +111 -0
- package/dist/services/persona-grouper.js.map +1 -0
- package/dist/services/persona-service.d.ts +52 -0
- package/dist/services/persona-service.d.ts.map +1 -0
- package/dist/services/{enhanced-persona-service.js → persona-service.js} +125 -7
- package/dist/services/persona-service.js.map +1 -0
- package/dist/services/remote/auth-token-service.d.ts.map +1 -1
- package/dist/services/remote/auth-token-service.js +10 -3
- package/dist/services/remote/auth-token-service.js.map +1 -1
- package/dist/services/remote/system-persona-service.d.ts.map +1 -1
- package/dist/services/remote/system-persona-service.js +41 -10
- package/dist/services/remote/system-persona-service.js.map +1 -1
- package/dist/services/repo-service.d.ts +195 -0
- package/dist/services/repo-service.d.ts.map +1 -0
- package/dist/services/repo-service.js +1023 -0
- package/dist/services/repo-service.js.map +1 -0
- package/dist/services/types/persona-types.d.ts +84 -0
- package/dist/services/types/persona-types.d.ts.map +1 -0
- package/dist/services/types/persona-types.js +5 -0
- package/dist/services/types/persona-types.js.map +1 -0
- package/dist/services/versioning-service.d.ts +79 -0
- package/dist/services/versioning-service.d.ts.map +1 -0
- package/dist/services/versioning-service.js +191 -0
- package/dist/services/versioning-service.js.map +1 -0
- package/dist/storage/local-filesystem-adapter.d.ts +1 -0
- package/dist/storage/local-filesystem-adapter.d.ts.map +1 -1
- package/dist/storage/local-filesystem-adapter.js +47 -3
- package/dist/storage/local-filesystem-adapter.js.map +1 -1
- package/dist/storage/platform-config-adapter.d.ts +9 -0
- package/dist/storage/platform-config-adapter.d.ts.map +1 -1
- package/dist/storage/platform-config-adapter.js +55 -1
- package/dist/storage/platform-config-adapter.js.map +1 -1
- package/dist/tools/analyse.tool.d.ts +17 -0
- package/dist/tools/analyse.tool.d.ts.map +1 -0
- package/dist/tools/analyse.tool.js +124 -0
- package/dist/tools/analyse.tool.js.map +1 -0
- package/dist/tools/persona/as.tool.d.ts +32 -11
- package/dist/tools/persona/as.tool.d.ts.map +1 -1
- package/dist/tools/persona/as.tool.js +452 -317
- package/dist/tools/persona/as.tool.js.map +1 -1
- package/dist/tools/persona/persona.tool.js +2 -2
- package/dist/tools/persona/persona.tool.js.map +1 -1
- package/dist/tools/plan/plan.tool.d.ts +3 -3
- package/dist/tools/plan/plan.tool.d.ts.map +1 -1
- package/dist/tools/plan/plan.tool.js +78 -55
- package/dist/tools/plan/plan.tool.js.map +1 -1
- package/dist/tools/tool-registry.d.ts.map +1 -1
- package/dist/tools/tool-registry.js +4 -0
- package/dist/tools/tool-registry.js.map +1 -1
- package/dist/utils/repo-utils.d.ts +10 -0
- package/dist/utils/repo-utils.d.ts.map +1 -0
- package/dist/utils/repo-utils.js +55 -0
- package/dist/utils/repo-utils.js.map +1 -0
- package/package.json +6 -2
- package/dist/services/enhanced-persona-service.d.ts +0 -22
- package/dist/services/enhanced-persona-service.d.ts.map +0 -1
- package/dist/services/enhanced-persona-service.js.map +0 -1
- package/dist/services/plan-watcher.service.d.ts +0 -141
- package/dist/services/plan-watcher.service.d.ts.map +0 -1
- package/dist/services/plan-watcher.service.js +0 -1010
- 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
|