@loxia-labs/loxia-autopilot-one 1.0.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 (80) hide show
  1. package/LICENSE +267 -0
  2. package/README.md +509 -0
  3. package/bin/cli.js +117 -0
  4. package/package.json +94 -0
  5. package/scripts/install-scanners.js +236 -0
  6. package/src/analyzers/CSSAnalyzer.js +297 -0
  7. package/src/analyzers/ConfigValidator.js +690 -0
  8. package/src/analyzers/ESLintAnalyzer.js +320 -0
  9. package/src/analyzers/JavaScriptAnalyzer.js +261 -0
  10. package/src/analyzers/PrettierFormatter.js +247 -0
  11. package/src/analyzers/PythonAnalyzer.js +266 -0
  12. package/src/analyzers/SecurityAnalyzer.js +729 -0
  13. package/src/analyzers/TypeScriptAnalyzer.js +247 -0
  14. package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
  15. package/src/analyzers/codeCloneDetector/detector.js +203 -0
  16. package/src/analyzers/codeCloneDetector/index.js +160 -0
  17. package/src/analyzers/codeCloneDetector/parser.js +199 -0
  18. package/src/analyzers/codeCloneDetector/reporter.js +148 -0
  19. package/src/analyzers/codeCloneDetector/scanner.js +59 -0
  20. package/src/core/agentPool.js +1474 -0
  21. package/src/core/agentScheduler.js +2147 -0
  22. package/src/core/contextManager.js +709 -0
  23. package/src/core/messageProcessor.js +732 -0
  24. package/src/core/orchestrator.js +548 -0
  25. package/src/core/stateManager.js +877 -0
  26. package/src/index.js +631 -0
  27. package/src/interfaces/cli.js +549 -0
  28. package/src/interfaces/webServer.js +2162 -0
  29. package/src/modules/fileExplorer/controller.js +280 -0
  30. package/src/modules/fileExplorer/index.js +37 -0
  31. package/src/modules/fileExplorer/middleware.js +92 -0
  32. package/src/modules/fileExplorer/routes.js +125 -0
  33. package/src/modules/fileExplorer/types.js +44 -0
  34. package/src/services/aiService.js +1232 -0
  35. package/src/services/apiKeyManager.js +164 -0
  36. package/src/services/benchmarkService.js +366 -0
  37. package/src/services/budgetService.js +539 -0
  38. package/src/services/contextInjectionService.js +247 -0
  39. package/src/services/conversationCompactionService.js +637 -0
  40. package/src/services/errorHandler.js +810 -0
  41. package/src/services/fileAttachmentService.js +544 -0
  42. package/src/services/modelRouterService.js +366 -0
  43. package/src/services/modelsService.js +322 -0
  44. package/src/services/qualityInspector.js +796 -0
  45. package/src/services/tokenCountingService.js +536 -0
  46. package/src/tools/agentCommunicationTool.js +1344 -0
  47. package/src/tools/agentDelayTool.js +485 -0
  48. package/src/tools/asyncToolManager.js +604 -0
  49. package/src/tools/baseTool.js +800 -0
  50. package/src/tools/browserTool.js +920 -0
  51. package/src/tools/cloneDetectionTool.js +621 -0
  52. package/src/tools/dependencyResolverTool.js +1215 -0
  53. package/src/tools/fileContentReplaceTool.js +875 -0
  54. package/src/tools/fileSystemTool.js +1107 -0
  55. package/src/tools/fileTreeTool.js +853 -0
  56. package/src/tools/imageTool.js +901 -0
  57. package/src/tools/importAnalyzerTool.js +1060 -0
  58. package/src/tools/jobDoneTool.js +248 -0
  59. package/src/tools/seekTool.js +956 -0
  60. package/src/tools/staticAnalysisTool.js +1778 -0
  61. package/src/tools/taskManagerTool.js +2873 -0
  62. package/src/tools/terminalTool.js +2304 -0
  63. package/src/tools/webTool.js +1430 -0
  64. package/src/types/agent.js +519 -0
  65. package/src/types/contextReference.js +972 -0
  66. package/src/types/conversation.js +730 -0
  67. package/src/types/toolCommand.js +747 -0
  68. package/src/utilities/attachmentValidator.js +292 -0
  69. package/src/utilities/configManager.js +582 -0
  70. package/src/utilities/constants.js +722 -0
  71. package/src/utilities/directoryAccessManager.js +535 -0
  72. package/src/utilities/fileProcessor.js +307 -0
  73. package/src/utilities/logger.js +436 -0
  74. package/src/utilities/tagParser.js +1246 -0
  75. package/src/utilities/toolConstants.js +317 -0
  76. package/web-ui/build/index.html +15 -0
  77. package/web-ui/build/logo.png +0 -0
  78. package/web-ui/build/logo2.png +0 -0
  79. package/web-ui/build/static/index-CjkkcnFA.js +344 -0
  80. package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
@@ -0,0 +1,877 @@
1
+ /**
2
+ * StateManager - Handles state persistence, recovery, and project state management
3
+ *
4
+ * Purpose:
5
+ * - Project state persistence and recovery
6
+ * - Agent state management across sessions
7
+ * - Multi-model conversation state handling
8
+ * - Context reference state management
9
+ * - Session recovery and resume functionality
10
+ */
11
+
12
+ import { promises as fs } from 'fs';
13
+ import path from 'path';
14
+
15
+ class StateManager {
16
+ constructor(config, logger) {
17
+ this.config = config;
18
+ this.logger = logger;
19
+
20
+ this.stateDirectory = config.system?.stateDirectory || '.loxia-state';
21
+ this.stateVersion = '1.0.0';
22
+
23
+ // State file paths
24
+ this.stateFiles = {
25
+ projectState: 'project-state.json',
26
+ agentIndex: 'agent-index.json',
27
+ conversationIndex: 'conversation-index.json',
28
+ lastSession: 'last-session.json',
29
+ contextReferences: 'context-references.json',
30
+ asyncOperations: 'operations/async-operations.json',
31
+ pausedAgents: 'operations/paused-agents.json',
32
+ toolHistory: 'operations/tool-history.json',
33
+ modelRouterCache: 'models/model-router-cache.json',
34
+ errorRecoveryLog: 'models/error-recovery-log.json'
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Initialize state directory structure
40
+ * @param {string} projectDir - Project directory path
41
+ * @returns {Promise<void>}
42
+ */
43
+ async initializeStateDirectory(projectDir) {
44
+ const stateDir = path.join(projectDir, this.stateDirectory);
45
+
46
+ try {
47
+ // Create main state directory
48
+ await fs.mkdir(stateDir, { recursive: true });
49
+
50
+ // Create subdirectories
51
+ await fs.mkdir(path.join(stateDir, 'agents'), { recursive: true });
52
+ await fs.mkdir(path.join(stateDir, 'operations'), { recursive: true });
53
+ await fs.mkdir(path.join(stateDir, 'models'), { recursive: true });
54
+
55
+ this.logger.info(`State directory initialized: ${stateDir}`);
56
+
57
+ } catch (error) {
58
+ this.logger.error(`Failed to initialize state directory: ${error.message}`);
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Resume project from saved state
65
+ * @param {string} projectDir - Project directory path
66
+ * @returns {Promise<Object>} Resumed project state
67
+ */
68
+ async resumeProject(projectDir) {
69
+ try {
70
+ await this.initializeStateDirectory(projectDir);
71
+
72
+ // Load project state
73
+ const projectState = await this.loadProjectState(projectDir);
74
+ const agentIndex = await this.loadAgentIndex(projectDir);
75
+
76
+ // Restore agents with multi-model conversations
77
+ const restoredAgents = [];
78
+ for (const [agentId, agentInfo] of Object.entries(agentIndex)) {
79
+ try {
80
+ const agent = await this.restoreAgent(agentId, agentInfo, projectDir);
81
+ restoredAgents.push(agent);
82
+ } catch (error) {
83
+ this.logger.warn(`Failed to restore agent: ${agentId}`, error.message);
84
+ }
85
+ }
86
+
87
+ // Restore async operations
88
+ const asyncOperations = await this.restoreAsyncOperations(projectDir);
89
+
90
+ // Restore paused agents
91
+ const pausedAgents = await this.restorePausedAgents(projectDir);
92
+
93
+ // Restore context references
94
+ const contextReferences = await this.restoreContextReferences(projectDir);
95
+
96
+ const resumedState = {
97
+ projectState,
98
+ agents: restoredAgents,
99
+ asyncOperations,
100
+ pausedAgents,
101
+ contextReferences,
102
+ resumedSuccessfully: true,
103
+ resumedAt: new Date().toISOString()
104
+ };
105
+
106
+ // Update last session
107
+ await this.saveLastSession(projectDir, {
108
+ resumedAt: new Date().toISOString(),
109
+ agentCount: restoredAgents.length,
110
+ operationCount: asyncOperations.length
111
+ });
112
+
113
+ this.logger.info(`Project resumed successfully`, {
114
+ projectDir,
115
+ agentCount: restoredAgents.length,
116
+ operationCount: asyncOperations.length
117
+ });
118
+
119
+ return resumedState;
120
+
121
+ } catch (error) {
122
+ this.logger.error(`Project resume failed: ${error.message}`, {
123
+ projectDir,
124
+ error: error.stack
125
+ });
126
+
127
+ return {
128
+ projectState: null,
129
+ agents: [],
130
+ asyncOperations: [],
131
+ pausedAgents: [],
132
+ contextReferences: [],
133
+ resumedSuccessfully: false,
134
+ error: error.message
135
+ };
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Persist agent state to storage
141
+ * @param {Object} agent - Agent object to persist
142
+ * @param {string} projectDir - Project directory path
143
+ * @returns {Promise<void>}
144
+ */
145
+ async persistAgentState(agent, projectDir = process.cwd()) {
146
+ const stateDir = path.join(projectDir, this.stateDirectory);
147
+ const agentStateFile = path.join(stateDir, 'agents', `agent-${agent.id}-state.json`);
148
+ const agentConversationsFile = path.join(stateDir, 'agents', `agent-${agent.id}-conversations.json`);
149
+
150
+ try {
151
+ // Separate conversations from main agent state
152
+ const { conversations, ...agentState } = agent;
153
+
154
+ // Save agent state
155
+ await this.saveJSON(agentStateFile, {
156
+ version: this.stateVersion,
157
+ agentId: agent.id,
158
+ state: agentState,
159
+ lastPersisted: new Date().toISOString()
160
+ });
161
+
162
+ // Save conversations separately
163
+ await this.saveJSON(agentConversationsFile, {
164
+ version: this.stateVersion,
165
+ agentId: agent.id,
166
+ conversations,
167
+ lastPersisted: new Date().toISOString()
168
+ });
169
+
170
+ // Update agent index
171
+ await this.updateAgentIndex(agent, projectDir);
172
+
173
+ this.logger.debug(`Agent state persisted: ${agent.id}`);
174
+
175
+ } catch (error) {
176
+ this.logger.error(`Failed to persist agent state: ${error.message}`, {
177
+ agentId: agent.id,
178
+ error: error.stack
179
+ });
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get project state
186
+ * @param {string} projectDir - Project directory path
187
+ * @returns {Promise<Object>} Project state object
188
+ */
189
+ async getProjectState(projectDir) {
190
+ return await this.loadProjectState(projectDir);
191
+ }
192
+
193
+ /**
194
+ * Load project state from storage
195
+ * @param {string} projectDir - Project directory path
196
+ * @returns {Promise<Object>} Project state object
197
+ */
198
+ async loadProjectState(projectDir) {
199
+ const stateFile = path.join(projectDir, this.stateDirectory, this.stateFiles.projectState);
200
+
201
+ try {
202
+ const data = await this.loadJSON(stateFile);
203
+ return data;
204
+ } catch (error) {
205
+ // Return default project state if file doesn't exist
206
+ const defaultState = {
207
+ version: this.stateVersion,
208
+ projectDir,
209
+ createdAt: new Date().toISOString(),
210
+ lastModified: new Date().toISOString(),
211
+ activeAgents: [],
212
+ lastActiveSession: null,
213
+ configuration: {
214
+ defaultModel: this.config.system?.defaultModel || 'anthropic-sonnet',
215
+ allowedTools: ['terminal', 'filesystem', 'browser'],
216
+ budgetLimit: 100.00
217
+ }
218
+ };
219
+
220
+ await this.saveProjectState(projectDir, defaultState);
221
+ return defaultState;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Save project state to storage
227
+ * @param {string} projectDir - Project directory path
228
+ * @param {Object} projectState - Project state object
229
+ * @returns {Promise<void>}
230
+ */
231
+ async saveProjectState(projectDir, projectState) {
232
+ const stateFile = path.join(projectDir, this.stateDirectory, this.stateFiles.projectState);
233
+
234
+ const stateData = {
235
+ ...projectState,
236
+ lastModified: new Date().toISOString()
237
+ };
238
+
239
+ await this.saveJSON(stateFile, stateData);
240
+ }
241
+
242
+ /**
243
+ * Load agent index
244
+ * @param {string} projectDir - Project directory path
245
+ * @returns {Promise<Object>} Agent index object
246
+ */
247
+ async loadAgentIndex(projectDir) {
248
+ const indexFile = path.join(projectDir, this.stateDirectory, this.stateFiles.agentIndex);
249
+
250
+ try {
251
+ return await this.loadJSON(indexFile);
252
+ } catch (error) {
253
+ return {}; // Return empty index if file doesn't exist
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Update agent index
259
+ * @param {Object} agent - Agent object
260
+ * @param {string} projectDir - Project directory path
261
+ * @returns {Promise<void>}
262
+ */
263
+ async updateAgentIndex(agent, projectDir) {
264
+ const indexFile = path.join(projectDir, this.stateDirectory, this.stateFiles.agentIndex);
265
+
266
+ let agentIndex;
267
+ try {
268
+ agentIndex = await this.loadJSON(indexFile);
269
+ } catch {
270
+ agentIndex = {};
271
+ }
272
+
273
+ agentIndex[agent.id] = {
274
+ name: agent.name,
275
+ type: agent.type,
276
+ stateFile: `agents/agent-${agent.id}-state.json`,
277
+ conversationsFile: `agents/agent-${agent.id}-conversations.json`,
278
+ lastActivity: agent.lastActivity,
279
+ model: agent.currentModel,
280
+ status: agent.status
281
+ };
282
+
283
+ await this.saveJSON(indexFile, agentIndex);
284
+ }
285
+
286
+ /**
287
+ * Restore agent from saved state
288
+ * @param {string} agentId - Agent identifier
289
+ * @param {Object} agentInfo - Agent info from index
290
+ * @param {string} projectDir - Project directory path
291
+ * @returns {Promise<Object>} Restored agent object
292
+ */
293
+ async restoreAgent(agentId, agentInfo, projectDir) {
294
+ const stateDir = path.join(projectDir, this.stateDirectory);
295
+ const stateFile = path.join(stateDir, agentInfo.stateFile);
296
+ const conversationsFile = path.join(stateDir, agentInfo.conversationsFile);
297
+
298
+ try {
299
+ // Load agent state
300
+ const stateData = await this.loadJSON(stateFile);
301
+ const conversationsData = await this.loadJSON(conversationsFile);
302
+
303
+ // Validate model conversations integrity
304
+ await this.validateModelConversations(conversationsData.conversations);
305
+
306
+ // Check if agent is paused
307
+ const pauseStatus = await this.checkAgentPauseStatus(agentId, projectDir);
308
+
309
+ const restoredAgent = {
310
+ ...stateData.state,
311
+ conversations: conversationsData.conversations,
312
+ isPaused: pauseStatus.isPaused,
313
+ pausedUntil: pauseStatus.pausedUntil,
314
+ isRestored: true,
315
+ restoredAt: new Date().toISOString()
316
+ };
317
+
318
+ this.logger.info(`Agent restored: ${agentId}`, {
319
+ name: restoredAgent.name,
320
+ status: restoredAgent.status,
321
+ messageCount: restoredAgent.conversations?.full?.messages?.length || 0
322
+ });
323
+
324
+ return restoredAgent;
325
+
326
+ } catch (error) {
327
+ this.logger.error(`Agent restoration failed: ${agentId}`, error.message);
328
+ throw error;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Restore async operations
334
+ * @param {string} projectDir - Project directory path
335
+ * @returns {Promise<Array>} Array of active async operations
336
+ */
337
+ async restoreAsyncOperations(projectDir) {
338
+ const operationsFile = path.join(projectDir, this.stateDirectory, this.stateFiles.asyncOperations);
339
+
340
+ try {
341
+ const data = await this.loadJSON(operationsFile);
342
+ return data.operations || [];
343
+ } catch {
344
+ return [];
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Restore paused agents
350
+ * @param {string} projectDir - Project directory path
351
+ * @returns {Promise<Object>} Paused agents data
352
+ */
353
+ async restorePausedAgents(projectDir) {
354
+ const pausedFile = path.join(projectDir, this.stateDirectory, this.stateFiles.pausedAgents);
355
+
356
+ try {
357
+ const data = await this.loadJSON(pausedFile);
358
+ const now = Date.now();
359
+
360
+ // Check which agents should be resumed
361
+ const toResume = [];
362
+ for (const [agentId, pauseInfo] of Object.entries(data.pausedAgents || {})) {
363
+ const pausedUntil = new Date(pauseInfo.pausedUntil).getTime();
364
+
365
+ if (now >= pausedUntil) {
366
+ toResume.push(agentId);
367
+ }
368
+ }
369
+
370
+ // Move expired pauses to history
371
+ for (const agentId of toResume) {
372
+ const pauseInfo = data.pausedAgents[agentId];
373
+ delete data.pausedAgents[agentId];
374
+
375
+ data.pauseHistory = data.pauseHistory || [];
376
+ data.pauseHistory.push({
377
+ agentId,
378
+ pausedAt: pauseInfo.pausedAt,
379
+ resumedAt: new Date().toISOString(),
380
+ reason: pauseInfo.reason,
381
+ actualDuration: Math.round((now - new Date(pauseInfo.pausedAt).getTime()) / 1000)
382
+ });
383
+ }
384
+
385
+ // Save updated data
386
+ await this.saveJSON(pausedFile, data);
387
+
388
+ return data;
389
+
390
+ } catch {
391
+ return {
392
+ pausedAgents: {},
393
+ pauseHistory: []
394
+ };
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Restore context references
400
+ * @param {string} projectDir - Project directory path
401
+ * @returns {Promise<Object>} Context references data
402
+ */
403
+ async restoreContextReferences(projectDir) {
404
+ const contextFile = path.join(projectDir, this.stateDirectory, this.stateFiles.contextReferences);
405
+
406
+ try {
407
+ const data = await this.loadJSON(contextFile);
408
+
409
+ // Validate context references (implementation would validate file existence, etc.)
410
+ const validatedReferences = [];
411
+ for (const reference of data.references || []) {
412
+ // Add validation logic here
413
+ reference.isValid = true; // Placeholder
414
+ reference.lastValidated = new Date().toISOString();
415
+ validatedReferences.push(reference);
416
+ }
417
+
418
+ data.references = validatedReferences;
419
+ await this.saveJSON(contextFile, data);
420
+
421
+ return data;
422
+
423
+ } catch {
424
+ return {
425
+ references: [],
426
+ lastCleanup: new Date().toISOString()
427
+ };
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Save last session data
433
+ * @param {string} projectDir - Project directory path
434
+ * @param {Object} sessionData - Session data to save
435
+ * @returns {Promise<void>}
436
+ */
437
+ async saveLastSession(projectDir, sessionData) {
438
+ const sessionFile = path.join(projectDir, this.stateDirectory, this.stateFiles.lastSession);
439
+
440
+ const data = {
441
+ ...sessionData,
442
+ savedAt: new Date().toISOString(),
443
+ projectDir
444
+ };
445
+
446
+ await this.saveJSON(sessionFile, data);
447
+ }
448
+
449
+ /**
450
+ * Load last session data
451
+ * @param {string} projectDir - Project directory path
452
+ * @returns {Promise<Object>} Last session data
453
+ */
454
+ async loadLastSession(projectDir) {
455
+ const sessionFile = path.join(projectDir, this.stateDirectory, this.stateFiles.lastSession);
456
+
457
+ try {
458
+ return await this.loadJSON(sessionFile);
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Save paused agent data
466
+ * @param {string} projectDir - Project directory path
467
+ * @param {string} agentId - Agent identifier
468
+ * @param {Object} pauseData - Pause information
469
+ * @returns {Promise<void>}
470
+ */
471
+ async savePausedAgent(projectDir, agentId, pauseData) {
472
+ const pausedFile = path.join(projectDir, this.stateDirectory, this.stateFiles.pausedAgents);
473
+
474
+ let data;
475
+ try {
476
+ data = await this.loadJSON(pausedFile);
477
+ } catch {
478
+ data = { pausedAgents: {}, pauseHistory: [] };
479
+ }
480
+
481
+ data.pausedAgents[agentId] = pauseData;
482
+ await this.saveJSON(pausedFile, data);
483
+ }
484
+
485
+ /**
486
+ * Remove paused agent data
487
+ * @param {string} projectDir - Project directory path
488
+ * @param {string} agentId - Agent identifier
489
+ * @returns {Promise<void>}
490
+ */
491
+ async removePausedAgent(projectDir, agentId) {
492
+ const pausedFile = path.join(projectDir, this.stateDirectory, this.stateFiles.pausedAgents);
493
+
494
+ try {
495
+ const data = await this.loadJSON(pausedFile);
496
+ delete data.pausedAgents[agentId];
497
+ await this.saveJSON(pausedFile, data);
498
+ } catch {
499
+ // File doesn't exist, nothing to remove
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Check agent pause status
505
+ * @param {string} agentId - Agent identifier
506
+ * @param {string} projectDir - Project directory path
507
+ * @returns {Promise<Object>} Pause status
508
+ */
509
+ async checkAgentPauseStatus(agentId, projectDir) {
510
+ const pausedFile = path.join(projectDir, this.stateDirectory, this.stateFiles.pausedAgents);
511
+
512
+ try {
513
+ const data = await this.loadJSON(pausedFile);
514
+ const pauseInfo = data.pausedAgents[agentId];
515
+
516
+ if (!pauseInfo) {
517
+ return { isPaused: false, pausedUntil: null };
518
+ }
519
+
520
+ const now = Date.now();
521
+ const pausedUntil = new Date(pauseInfo.pausedUntil).getTime();
522
+
523
+ return {
524
+ isPaused: now < pausedUntil,
525
+ pausedUntil: pauseInfo.pausedUntil,
526
+ reason: pauseInfo.reason
527
+ };
528
+
529
+ } catch {
530
+ return { isPaused: false, pausedUntil: null };
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Validate model conversations integrity
536
+ * @param {Object} conversations - Conversations object
537
+ * @returns {Promise<void>}
538
+ */
539
+ async validateModelConversations(conversations) {
540
+ if (!conversations || !conversations.full) {
541
+ throw new Error('Invalid conversations structure - missing full conversation');
542
+ }
543
+
544
+ const fullMessages = conversations.full.messages || [];
545
+ const fullLastUpdated = new Date(conversations.full.lastUpdated);
546
+
547
+ // Validate each model conversation against full conversation
548
+ for (const [modelName, modelConv] of Object.entries(conversations)) {
549
+ if (modelName === 'full') continue;
550
+
551
+ if (!modelConv.messages) {
552
+ this.logger.warn(`Model conversation ${modelName} missing messages array`);
553
+ continue;
554
+ }
555
+
556
+ const modelLastUpdated = new Date(modelConv.lastUpdated);
557
+
558
+ if (fullLastUpdated > modelLastUpdated) {
559
+ this.logger.warn(`Model conversation ${modelName} is outdated, will sync on next use`);
560
+ modelConv.needsSync = true;
561
+ }
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Save JSON data to file
567
+ * @private
568
+ */
569
+ async saveJSON(filePath, data) {
570
+ const dir = path.dirname(filePath);
571
+ await fs.mkdir(dir, { recursive: true });
572
+
573
+ const jsonData = JSON.stringify(data, null, 2);
574
+ await fs.writeFile(filePath, jsonData, 'utf8');
575
+ }
576
+
577
+ /**
578
+ * Load JSON data from file
579
+ * @private
580
+ */
581
+ async loadJSON(filePath) {
582
+ const data = await fs.readFile(filePath, 'utf8');
583
+ return JSON.parse(data);
584
+ }
585
+
586
+ /**
587
+ * Delete agent state from storage
588
+ * @param {string} agentId - Agent identifier
589
+ * @param {string} projectDir - Project directory path
590
+ * @returns {Promise<void>}
591
+ */
592
+ async deleteAgentState(agentId, projectDir = process.cwd()) {
593
+ const stateDir = path.join(projectDir, this.stateDirectory);
594
+ const agentStateFile = path.join(stateDir, 'agents', `agent-${agentId}-state.json`);
595
+ const agentConversationsFile = path.join(stateDir, 'agents', `agent-${agentId}-conversations.json`);
596
+
597
+ try {
598
+ // Delete agent state file
599
+ try {
600
+ await fs.unlink(agentStateFile);
601
+ this.logger.debug(`Deleted agent state file: ${agentId}`);
602
+ } catch (error) {
603
+ if (error.code !== 'ENOENT') {
604
+ this.logger.warn(`Failed to delete agent state file: ${error.message}`, { agentId });
605
+ }
606
+ }
607
+
608
+ // Delete agent conversations file
609
+ try {
610
+ await fs.unlink(agentConversationsFile);
611
+ this.logger.debug(`Deleted agent conversations file: ${agentId}`);
612
+ } catch (error) {
613
+ if (error.code !== 'ENOENT') {
614
+ this.logger.warn(`Failed to delete agent conversations file: ${error.message}`, { agentId });
615
+ }
616
+ }
617
+
618
+ // Remove from agent index
619
+ await this.removeFromAgentIndex(agentId, projectDir);
620
+
621
+ this.logger.info(`Agent state deleted: ${agentId}`);
622
+
623
+ } catch (error) {
624
+ this.logger.error(`Failed to delete agent state: ${error.message}`, {
625
+ agentId,
626
+ error: error.stack
627
+ });
628
+ throw error;
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Remove agent from agent index
634
+ * @param {string} agentId - Agent identifier
635
+ * @param {string} projectDir - Project directory path
636
+ * @returns {Promise<void>}
637
+ */
638
+ async removeFromAgentIndex(agentId, projectDir) {
639
+ const indexFile = path.join(projectDir, this.stateDirectory, this.stateFiles.agentIndex);
640
+
641
+ try {
642
+ const agentIndex = await this.loadJSON(indexFile);
643
+ delete agentIndex[agentId];
644
+ await this.saveJSON(indexFile, agentIndex);
645
+ this.logger.debug(`Removed agent from index: ${agentId}`);
646
+ } catch (error) {
647
+ // If index doesn't exist or can't be updated, log but don't throw
648
+ this.logger.warn(`Failed to remove agent from index: ${error.message}`, { agentId });
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Check if state directory exists
654
+ * @param {string} projectDir - Project directory path
655
+ * @returns {Promise<boolean>} True if state directory exists
656
+ */
657
+ async stateDirectoryExists(projectDir) {
658
+ const stateDir = path.join(projectDir, this.stateDirectory);
659
+
660
+ try {
661
+ const stats = await fs.stat(stateDir);
662
+ return stats.isDirectory();
663
+ } catch {
664
+ return false;
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Clean up old state files
670
+ * @param {string} projectDir - Project directory path
671
+ * @param {number} maxAge - Maximum age in days
672
+ * @returns {Promise<void>}
673
+ */
674
+ async cleanupOldState(projectDir, maxAge = 30) {
675
+ const stateDir = path.join(projectDir, this.stateDirectory);
676
+ const cutoffDate = Date.now() - (maxAge * 24 * 60 * 60 * 1000);
677
+
678
+ try {
679
+ const agentsDir = path.join(stateDir, 'agents');
680
+ const files = await fs.readdir(agentsDir);
681
+
682
+ for (const file of files) {
683
+ const filePath = path.join(agentsDir, file);
684
+ const stats = await fs.stat(filePath);
685
+
686
+ if (stats.mtime.getTime() < cutoffDate) {
687
+ await fs.unlink(filePath);
688
+ this.logger.info(`Cleaned up old state file: ${file}`);
689
+ }
690
+ }
691
+
692
+ } catch (error) {
693
+ this.logger.warn(`State cleanup failed: ${error.message}`);
694
+ }
695
+ }
696
+
697
+ /**
698
+ * Get all available agents (active + archived) from filesystem
699
+ * @param {string} projectDir - Project directory path
700
+ * @param {Object} agentPool - Agent pool instance to check active agents
701
+ * @returns {Promise<Array>} List of all agents with metadata
702
+ */
703
+ async getAllAvailableAgents(projectDir, agentPool) {
704
+ try {
705
+ const agentIndex = await this.loadAgentIndex(projectDir);
706
+ const activeAgentIds = agentPool ? (await agentPool.getAllAgents()).map(a => a.id) : [];
707
+
708
+ const agents = [];
709
+ for (const [agentId, info] of Object.entries(agentIndex)) {
710
+ // Skip invalid or undefined entries
711
+ if (!info || !info.name) {
712
+ continue;
713
+ }
714
+
715
+ agents.push({
716
+ agentId,
717
+ name: info.name,
718
+ type: info.type,
719
+ model: info.model,
720
+ lastActivity: info.lastActivity,
721
+ status: info.status,
722
+ stateFile: info.stateFile,
723
+ conversationsFile: info.conversationsFile,
724
+ isLoaded: activeAgentIds.includes(agentId),
725
+ canImport: !activeAgentIds.includes(agentId)
726
+ });
727
+ }
728
+
729
+ // Sort by last activity (most recent first)
730
+ agents.sort((a, b) => {
731
+ const dateA = a.lastActivity ? new Date(a.lastActivity) : new Date(0);
732
+ const dateB = b.lastActivity ? new Date(b.lastActivity) : new Date(0);
733
+ return dateB - dateA;
734
+ });
735
+
736
+ this.logger.info(`Found ${agents.length} available agents (${agents.filter(a => a.isLoaded).length} active, ${agents.filter(a => !a.isLoaded).length} archived)`);
737
+
738
+ return agents;
739
+ } catch (error) {
740
+ this.logger.error(`Failed to get available agents: ${error.message}`);
741
+ throw error;
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Get agent metadata without full restoration (lightweight preview)
747
+ * @param {string} agentId - Agent ID
748
+ * @param {string} projectDir - Project directory path
749
+ * @returns {Promise<Object>} Agent metadata for preview
750
+ */
751
+ async getAgentMetadata(agentId, projectDir) {
752
+ try {
753
+ // Load agent index
754
+ const agentIndex = await this.loadAgentIndex(projectDir);
755
+ const agentInfo = agentIndex[agentId];
756
+
757
+ if (!agentInfo) {
758
+ throw new Error(`Agent ${agentId} not found in index`);
759
+ }
760
+
761
+ // Load just the state file (lightweight)
762
+ const stateDir = path.join(projectDir, this.stateDirectory);
763
+ const stateFile = path.join(stateDir, agentInfo.stateFile);
764
+ const conversationsFile = path.join(stateDir, agentInfo.conversationsFile);
765
+
766
+ // Check if files exist
767
+ const stateExists = await fs.access(stateFile).then(() => true).catch(() => false);
768
+ const conversationsExist = await fs.access(conversationsFile).then(() => true).catch(() => false);
769
+
770
+ if (!stateExists) {
771
+ throw new Error(`State file not found for agent ${agentId}`);
772
+ }
773
+
774
+ // Load state
775
+ const stateData = await this.loadJSON(stateFile);
776
+ const state = stateData.state || {};
777
+
778
+ // Load conversation count without loading full messages (for performance)
779
+ let messageCount = 0;
780
+ let lastMessage = null;
781
+ if (conversationsExist) {
782
+ try {
783
+ const conversations = await this.loadJSON(conversationsFile);
784
+ messageCount = Array.isArray(conversations) ? conversations.length : 0;
785
+
786
+ // Get last message for preview
787
+ if (messageCount > 0) {
788
+ const lastMsg = conversations[conversations.length - 1];
789
+ lastMessage = lastMsg?.content?.substring(0, 100) || null;
790
+ }
791
+ } catch (error) {
792
+ this.logger.warn(`Failed to load conversations for ${agentId}: ${error.message}`);
793
+ }
794
+ }
795
+
796
+ const metadata = {
797
+ agentId,
798
+ name: agentInfo.name || state.name,
799
+ model: agentInfo.model || state.preferredModel || state.currentModel,
800
+ lastActivity: agentInfo.lastActivity,
801
+ status: agentInfo.status,
802
+ capabilities: state.capabilities || [],
803
+ messageCount,
804
+ lastMessage,
805
+ taskCount: state.taskList?.tasks?.length || 0,
806
+ createdAt: state.createdAt,
807
+ workingDirectory: state.directoryAccess?.workingDirectory,
808
+ mode: state.mode,
809
+ systemPrompt: state.originalSystemPrompt
810
+ };
811
+
812
+ this.logger.info(`Loaded metadata for agent ${agentId}: ${metadata.messageCount} messages, ${metadata.taskCount} tasks`);
813
+
814
+ return metadata;
815
+ } catch (error) {
816
+ this.logger.error(`Failed to get agent metadata for ${agentId}: ${error.message}`);
817
+ throw error;
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Import archived agent from filesystem and add to agent pool
823
+ * @param {string} agentId - Agent ID to import
824
+ * @param {string} projectDir - Project directory path
825
+ * @param {Object} agentPool - Agent pool instance
826
+ * @returns {Promise<Object>} Imported agent object
827
+ */
828
+ async importArchivedAgent(agentId, projectDir, agentPool) {
829
+ try {
830
+ // Validate agent ID format for security
831
+ const AGENT_ID_REGEX = /^agent-[a-z0-9-]+-\d+$/;
832
+ if (!AGENT_ID_REGEX.test(agentId)) {
833
+ throw new Error('Invalid agent ID format');
834
+ }
835
+
836
+ // Check if already loaded in agent pool
837
+ if (agentPool && await agentPool.getAgent(agentId)) {
838
+ throw new Error(`Agent ${agentId} is already loaded. Use switchAgent() instead.`);
839
+ }
840
+
841
+ // Load from agent index
842
+ const agentIndex = await this.loadAgentIndex(projectDir);
843
+ const agentInfo = agentIndex[agentId];
844
+
845
+ if (!agentInfo) {
846
+ throw new Error(`Agent ${agentId} not found in index`);
847
+ }
848
+
849
+ this.logger.info(`Importing archived agent: ${agentId} (${agentInfo.name})`);
850
+
851
+ // Restore agent using existing restore logic
852
+ const agent = await this.restoreAgent(agentId, agentInfo, projectDir);
853
+
854
+ // Update agent's last activity
855
+ agent.lastActivity = new Date().toISOString();
856
+
857
+ // Add to agent pool if provided
858
+ if (agentPool) {
859
+ agentPool.agents.set(agent.id, agent);
860
+ agentPool._updateAgentDirectory(agent);
861
+ this.logger.info(`Agent ${agentId} added to agent pool`);
862
+ }
863
+
864
+ // Update agent index with new last activity
865
+ await this.updateAgentIndex(agent, projectDir);
866
+
867
+ this.logger.info(`Successfully imported agent ${agentId}: ${agent.name}`);
868
+
869
+ return agent;
870
+ } catch (error) {
871
+ this.logger.error(`Failed to import agent ${agentId}: ${error.message}`);
872
+ throw error;
873
+ }
874
+ }
875
+ }
876
+
877
+ export default StateManager;