@wundr.io/cli 1.0.0

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 (213) hide show
  1. package/README.md +551 -0
  2. package/bin/wundr.js +39 -0
  3. package/dist/ai/ai-service.d.ts +152 -0
  4. package/dist/ai/ai-service.d.ts.map +1 -0
  5. package/dist/ai/ai-service.js +430 -0
  6. package/dist/ai/ai-service.js.map +1 -0
  7. package/dist/ai/claude-client.d.ts +130 -0
  8. package/dist/ai/claude-client.d.ts.map +1 -0
  9. package/dist/ai/claude-client.js +339 -0
  10. package/dist/ai/claude-client.js.map +1 -0
  11. package/dist/ai/conversation-manager.d.ts +164 -0
  12. package/dist/ai/conversation-manager.d.ts.map +1 -0
  13. package/dist/ai/conversation-manager.js +612 -0
  14. package/dist/ai/conversation-manager.js.map +1 -0
  15. package/dist/ai/index.d.ts +5 -0
  16. package/dist/ai/index.d.ts.map +1 -0
  17. package/dist/ai/index.js +8 -0
  18. package/dist/ai/index.js.map +1 -0
  19. package/dist/cli.d.ts +36 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +173 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/commands/ai.d.ts +89 -0
  24. package/dist/commands/ai.d.ts.map +1 -0
  25. package/dist/commands/ai.js +735 -0
  26. package/dist/commands/ai.js.map +1 -0
  27. package/dist/commands/analyze-optimized.d.ts +14 -0
  28. package/dist/commands/analyze-optimized.d.ts.map +1 -0
  29. package/dist/commands/analyze-optimized.js +437 -0
  30. package/dist/commands/analyze-optimized.js.map +1 -0
  31. package/dist/commands/analyze.d.ts +65 -0
  32. package/dist/commands/analyze.d.ts.map +1 -0
  33. package/dist/commands/analyze.js +435 -0
  34. package/dist/commands/analyze.js.map +1 -0
  35. package/dist/commands/batch.d.ts +71 -0
  36. package/dist/commands/batch.d.ts.map +1 -0
  37. package/dist/commands/batch.js +738 -0
  38. package/dist/commands/batch.js.map +1 -0
  39. package/dist/commands/chat.d.ts +71 -0
  40. package/dist/commands/chat.d.ts.map +1 -0
  41. package/dist/commands/chat.js +674 -0
  42. package/dist/commands/chat.js.map +1 -0
  43. package/dist/commands/claude-init.d.ts +28 -0
  44. package/dist/commands/claude-init.d.ts.map +1 -0
  45. package/dist/commands/claude-init.js +587 -0
  46. package/dist/commands/claude-init.js.map +1 -0
  47. package/dist/commands/claude-setup.d.ts +32 -0
  48. package/dist/commands/claude-setup.d.ts.map +1 -0
  49. package/dist/commands/claude-setup.js +570 -0
  50. package/dist/commands/claude-setup.js.map +1 -0
  51. package/dist/commands/computer-setup-commands.d.ts +39 -0
  52. package/dist/commands/computer-setup-commands.d.ts.map +1 -0
  53. package/dist/commands/computer-setup-commands.js +563 -0
  54. package/dist/commands/computer-setup-commands.js.map +1 -0
  55. package/dist/commands/computer-setup.d.ts +7 -0
  56. package/dist/commands/computer-setup.d.ts.map +1 -0
  57. package/dist/commands/computer-setup.js +481 -0
  58. package/dist/commands/computer-setup.js.map +1 -0
  59. package/dist/commands/create-command.d.ts +7 -0
  60. package/dist/commands/create-command.d.ts.map +1 -0
  61. package/dist/commands/create-command.js +158 -0
  62. package/dist/commands/create-command.js.map +1 -0
  63. package/dist/commands/create.d.ts +74 -0
  64. package/dist/commands/create.d.ts.map +1 -0
  65. package/dist/commands/create.js +556 -0
  66. package/dist/commands/create.js.map +1 -0
  67. package/dist/commands/dashboard.d.ts +91 -0
  68. package/dist/commands/dashboard.d.ts.map +1 -0
  69. package/dist/commands/dashboard.js +537 -0
  70. package/dist/commands/dashboard.js.map +1 -0
  71. package/dist/commands/govern.d.ts +70 -0
  72. package/dist/commands/govern.d.ts.map +1 -0
  73. package/dist/commands/govern.js +480 -0
  74. package/dist/commands/govern.js.map +1 -0
  75. package/dist/commands/init.d.ts +55 -0
  76. package/dist/commands/init.d.ts.map +1 -0
  77. package/dist/commands/init.js +584 -0
  78. package/dist/commands/init.js.map +1 -0
  79. package/dist/commands/performance-optimizer.d.ts +30 -0
  80. package/dist/commands/performance-optimizer.d.ts.map +1 -0
  81. package/dist/commands/performance-optimizer.js +649 -0
  82. package/dist/commands/performance-optimizer.js.map +1 -0
  83. package/dist/commands/plugins.d.ts +87 -0
  84. package/dist/commands/plugins.d.ts.map +1 -0
  85. package/dist/commands/plugins.js +685 -0
  86. package/dist/commands/plugins.js.map +1 -0
  87. package/dist/commands/setup.d.ts +29 -0
  88. package/dist/commands/setup.d.ts.map +1 -0
  89. package/dist/commands/setup.js +399 -0
  90. package/dist/commands/setup.js.map +1 -0
  91. package/dist/commands/test-init.d.ts +9 -0
  92. package/dist/commands/test-init.d.ts.map +1 -0
  93. package/dist/commands/test-init.js +222 -0
  94. package/dist/commands/test-init.js.map +1 -0
  95. package/dist/commands/test.d.ts +25 -0
  96. package/dist/commands/test.d.ts.map +1 -0
  97. package/dist/commands/test.js +217 -0
  98. package/dist/commands/test.js.map +1 -0
  99. package/dist/commands/watch.d.ts +76 -0
  100. package/dist/commands/watch.d.ts.map +1 -0
  101. package/dist/commands/watch.js +610 -0
  102. package/dist/commands/watch.js.map +1 -0
  103. package/dist/context/context-manager.d.ts +155 -0
  104. package/dist/context/context-manager.d.ts.map +1 -0
  105. package/dist/context/context-manager.js +383 -0
  106. package/dist/context/context-manager.js.map +1 -0
  107. package/dist/context/index.d.ts +3 -0
  108. package/dist/context/index.d.ts.map +1 -0
  109. package/dist/context/index.js +6 -0
  110. package/dist/context/index.js.map +1 -0
  111. package/dist/context/session-manager.d.ts +207 -0
  112. package/dist/context/session-manager.d.ts.map +1 -0
  113. package/dist/context/session-manager.js +682 -0
  114. package/dist/context/session-manager.js.map +1 -0
  115. package/dist/index.d.ts +8 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +51 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/interactive/interactive-mode.d.ts +76 -0
  120. package/dist/interactive/interactive-mode.d.ts.map +1 -0
  121. package/dist/interactive/interactive-mode.js +730 -0
  122. package/dist/interactive/interactive-mode.js.map +1 -0
  123. package/dist/nlp/command-mapper.d.ts +174 -0
  124. package/dist/nlp/command-mapper.d.ts.map +1 -0
  125. package/dist/nlp/command-mapper.js +623 -0
  126. package/dist/nlp/command-mapper.js.map +1 -0
  127. package/dist/nlp/command-parser.d.ts +106 -0
  128. package/dist/nlp/command-parser.d.ts.map +1 -0
  129. package/dist/nlp/command-parser.js +416 -0
  130. package/dist/nlp/command-parser.js.map +1 -0
  131. package/dist/nlp/index.d.ts +5 -0
  132. package/dist/nlp/index.d.ts.map +1 -0
  133. package/dist/nlp/index.js +8 -0
  134. package/dist/nlp/index.js.map +1 -0
  135. package/dist/nlp/intent-classifier.d.ts +59 -0
  136. package/dist/nlp/intent-classifier.d.ts.map +1 -0
  137. package/dist/nlp/intent-classifier.js +384 -0
  138. package/dist/nlp/intent-classifier.js.map +1 -0
  139. package/dist/nlp/intent-parser.d.ts +152 -0
  140. package/dist/nlp/intent-parser.d.ts.map +1 -0
  141. package/dist/nlp/intent-parser.js +739 -0
  142. package/dist/nlp/intent-parser.js.map +1 -0
  143. package/dist/plugins/plugin-manager.d.ts +120 -0
  144. package/dist/plugins/plugin-manager.d.ts.map +1 -0
  145. package/dist/plugins/plugin-manager.js +595 -0
  146. package/dist/plugins/plugin-manager.js.map +1 -0
  147. package/dist/types/index.d.ts +224 -0
  148. package/dist/types/index.d.ts.map +1 -0
  149. package/dist/types/index.js +3 -0
  150. package/dist/types/index.js.map +1 -0
  151. package/dist/utils/config-manager.d.ts +73 -0
  152. package/dist/utils/config-manager.d.ts.map +1 -0
  153. package/dist/utils/config-manager.js +339 -0
  154. package/dist/utils/config-manager.js.map +1 -0
  155. package/dist/utils/error-handler.d.ts +46 -0
  156. package/dist/utils/error-handler.d.ts.map +1 -0
  157. package/dist/utils/error-handler.js +169 -0
  158. package/dist/utils/error-handler.js.map +1 -0
  159. package/dist/utils/logger.d.ts +25 -0
  160. package/dist/utils/logger.d.ts.map +1 -0
  161. package/dist/utils/logger.js +94 -0
  162. package/dist/utils/logger.js.map +1 -0
  163. package/package.json +119 -0
  164. package/src/ai/ai-service.ts +595 -0
  165. package/src/ai/claude-client.ts +490 -0
  166. package/src/ai/conversation-manager.ts +907 -0
  167. package/src/ai/index.ts +8 -0
  168. package/src/cli.ts +202 -0
  169. package/src/commands/ai.ts +995 -0
  170. package/src/commands/analyze-optimized.ts +641 -0
  171. package/src/commands/analyze.ts +576 -0
  172. package/src/commands/batch.ts +935 -0
  173. package/src/commands/chat.ts +876 -0
  174. package/src/commands/claude-init.ts +715 -0
  175. package/src/commands/claude-setup.ts +697 -0
  176. package/src/commands/computer-setup-commands.ts +709 -0
  177. package/src/commands/computer-setup.ts +565 -0
  178. package/src/commands/create-command.ts +175 -0
  179. package/src/commands/create.ts +727 -0
  180. package/src/commands/dashboard.ts +691 -0
  181. package/src/commands/govern.ts +635 -0
  182. package/src/commands/init.ts +677 -0
  183. package/src/commands/performance-optimizer.ts +864 -0
  184. package/src/commands/plugins.ts +848 -0
  185. package/src/commands/setup.ts +508 -0
  186. package/src/commands/test-init.ts +242 -0
  187. package/src/commands/test.ts +264 -0
  188. package/src/commands/watch.ts +755 -0
  189. package/src/context/context-manager.ts +546 -0
  190. package/src/context/index.ts +9 -0
  191. package/src/context/session-manager.ts +1019 -0
  192. package/src/index.ts +64 -0
  193. package/src/interactive/interactive-mode.ts +830 -0
  194. package/src/nlp/command-mapper.ts +885 -0
  195. package/src/nlp/command-parser.ts +564 -0
  196. package/src/nlp/index.ts +4 -0
  197. package/src/nlp/intent-classifier.ts +458 -0
  198. package/src/nlp/intent-parser.ts +1101 -0
  199. package/src/plugins/plugin-manager.ts +744 -0
  200. package/src/types/index.ts +252 -0
  201. package/src/types/modules.d.ts +56 -0
  202. package/src/utils/config-manager.ts +391 -0
  203. package/src/utils/error-handler.ts +192 -0
  204. package/src/utils/logger.ts +104 -0
  205. package/templates/batch/ci-cd.yaml +62 -0
  206. package/templates/component/{{fileName}}.test.tsx +17 -0
  207. package/templates/component/{{fileName}}.tsx +21 -0
  208. package/templates/service/{{fileName}}.ts +98 -0
  209. package/templates/wundr-test.config.js +0 -0
  210. package/test-suites/api/health.spec.ts +134 -0
  211. package/test-suites/helpers/test-config.ts +84 -0
  212. package/test-suites/ui/accessibility.spec.ts +102 -0
  213. package/test-suites/ui/smoke.spec.ts +92 -0
@@ -0,0 +1,1019 @@
1
+ import { EventEmitter } from 'events';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { logger } from '../utils/logger';
5
+ import { ConversationManager } from '../ai/conversation-manager';
6
+ import { CommandMapper } from '../nlp/command-mapper';
7
+
8
+ /**
9
+ * User preferences for personalized experience
10
+ */
11
+ export interface UserPreferences {
12
+ defaultModel: string;
13
+ preferredOutputFormat: 'json' | 'table' | 'csv';
14
+ confirmDestructiveCommands: boolean;
15
+ enableStreamingResponses: boolean;
16
+ maxHistoryLength: number;
17
+ autoSave: boolean;
18
+ theme: 'light' | 'dark' | 'auto';
19
+ verbosity: 'minimal' | 'normal' | 'verbose';
20
+ aliases: Record<string, string>;
21
+ favorites: string[];
22
+ workspacePreferences: Record<string, any>;
23
+ }
24
+
25
+ /**
26
+ * Project context information
27
+ */
28
+ export interface ProjectContext {
29
+ rootPath: string;
30
+ projectType:
31
+ | 'nodejs'
32
+ | 'react'
33
+ | 'vue'
34
+ | 'angular'
35
+ | 'python'
36
+ | 'java'
37
+ | 'unknown';
38
+ packageManager:
39
+ | 'npm'
40
+ | 'yarn'
41
+ | 'pnpm'
42
+ | 'pip'
43
+ | 'maven'
44
+ | 'gradle'
45
+ | 'unknown';
46
+ gitRepository?: {
47
+ remote: string;
48
+ branch: string;
49
+ status: string;
50
+ hasUncommittedChanges: boolean;
51
+ };
52
+ dependencies: string[];
53
+ devDependencies: string[];
54
+ scripts: Record<string, string>;
55
+ lastAnalysis?: {
56
+ timestamp: Date;
57
+ summary: Record<string, any>;
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Session state for maintaining context
63
+ */
64
+ export interface SessionState {
65
+ id: string;
66
+ userId: string;
67
+ currentWorkspace: string;
68
+ activeConversation?: string;
69
+ recentCommands: Array<{
70
+ command: string;
71
+ timestamp: Date;
72
+ success: boolean;
73
+ duration: number;
74
+ }>;
75
+ contextStack: Array<{
76
+ type: 'project' | 'command' | 'conversation';
77
+ context: any;
78
+ timestamp: Date;
79
+ }>;
80
+ userPreferences: UserPreferences;
81
+ projectContext?: ProjectContext;
82
+ temporaryData: Record<string, any>;
83
+ created: Date;
84
+ lastAccessed: Date;
85
+ }
86
+
87
+ /**
88
+ * Workspace configuration
89
+ */
90
+ export interface WorkspaceConfig {
91
+ name: string;
92
+ path: string;
93
+ type: string;
94
+ settings: Record<string, any>;
95
+ lastAccessed: Date;
96
+ bookmarked: boolean;
97
+ }
98
+
99
+ /**
100
+ * Session manager configuration
101
+ */
102
+ export interface SessionManagerConfig {
103
+ persistencePath: string;
104
+ sessionTimeout: number; // minutes
105
+ maxSessions: number;
106
+ autoDetectProjects: boolean;
107
+ enableContextLearning: boolean;
108
+ backupInterval: number; // minutes
109
+ }
110
+
111
+ /**
112
+ * Session manager for persistent context, user preferences, and workspace management
113
+ */
114
+ export class SessionManager extends EventEmitter {
115
+ private config: SessionManagerConfig;
116
+ private conversationManager: ConversationManager;
117
+ private commandMapper: CommandMapper;
118
+ private activeSessions: Map<string, SessionState>;
119
+ private workspaces: Map<string, WorkspaceConfig>;
120
+ private currentSessionId?: string;
121
+ private backupTimer?: NodeJS.Timeout;
122
+ private cleanupTimer?: NodeJS.Timeout;
123
+
124
+ constructor(
125
+ conversationManager: ConversationManager,
126
+ commandMapper: CommandMapper,
127
+ config: Partial<SessionManagerConfig> = {}
128
+ ) {
129
+ super();
130
+
131
+ this.conversationManager = conversationManager;
132
+ this.commandMapper = commandMapper;
133
+ this.config = {
134
+ persistencePath: path.join(process.cwd(), '.wundr', 'sessions'),
135
+ sessionTimeout: 24 * 60, // 24 hours
136
+ maxSessions: 10,
137
+ autoDetectProjects: true,
138
+ enableContextLearning: true,
139
+ backupInterval: 15, // 15 minutes
140
+ ...config,
141
+ };
142
+
143
+ this.activeSessions = new Map();
144
+ this.workspaces = new Map();
145
+
146
+ this.initialize();
147
+ }
148
+
149
+ /**
150
+ * Create or resume a session
151
+ */
152
+ async createSession(
153
+ userId: string,
154
+ workspacePath?: string,
155
+ sessionId?: string
156
+ ): Promise<string> {
157
+ const id =
158
+ sessionId ||
159
+ `session_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
160
+
161
+ // Check if session already exists
162
+ if (this.activeSessions.has(id)) {
163
+ await this.resumeSession(id);
164
+ return id;
165
+ }
166
+
167
+ const workspace = workspacePath || process.cwd();
168
+ const projectContext = await this.detectProjectContext(workspace);
169
+
170
+ const session: SessionState = {
171
+ id,
172
+ userId,
173
+ currentWorkspace: workspace,
174
+ recentCommands: [],
175
+ contextStack: [],
176
+ userPreferences: await this.loadUserPreferences(userId),
177
+ projectContext,
178
+ temporaryData: {},
179
+ created: new Date(),
180
+ lastAccessed: new Date(),
181
+ };
182
+
183
+ // Load workspace if it exists
184
+ const workspaceConfig = await this.loadWorkspace(workspace);
185
+ if (workspaceConfig) {
186
+ workspaceConfig.lastAccessed = new Date();
187
+ this.workspaces.set(workspace, workspaceConfig);
188
+ }
189
+
190
+ this.activeSessions.set(id, session);
191
+ this.currentSessionId = id;
192
+
193
+ await this.persistSession(session);
194
+
195
+ this.emit('session_created', { sessionId: id, userId, workspace });
196
+ logger.debug(`Created session: ${id} for user: ${userId}`);
197
+
198
+ return id;
199
+ }
200
+
201
+ /**
202
+ * Resume an existing session
203
+ */
204
+ async resumeSession(sessionId: string): Promise<SessionState> {
205
+ let session = this.activeSessions.get(sessionId);
206
+
207
+ if (!session) {
208
+ // Try to load from persistence
209
+ session = (await this.loadSession(sessionId)) || undefined;
210
+ if (!session) {
211
+ throw new Error(`Session not found: ${sessionId}`);
212
+ }
213
+ }
214
+
215
+ session.lastAccessed = new Date();
216
+ this.currentSessionId = sessionId;
217
+ this.activeSessions.set(sessionId, session);
218
+
219
+ // Refresh project context
220
+ if (session.projectContext) {
221
+ session.projectContext = await this.detectProjectContext(
222
+ session.currentWorkspace
223
+ );
224
+ }
225
+
226
+ await this.persistSession(session);
227
+
228
+ this.emit('session_resumed', { sessionId, session });
229
+ logger.debug(`Resumed session: ${sessionId}`);
230
+
231
+ return session;
232
+ }
233
+
234
+ /**
235
+ * Get current session
236
+ */
237
+ getCurrentSession(): SessionState | null {
238
+ if (!this.currentSessionId) return null;
239
+ return this.activeSessions.get(this.currentSessionId) || null;
240
+ }
241
+
242
+ /**
243
+ * Get session by ID
244
+ */
245
+ getSession(sessionId: string): SessionState | null {
246
+ return this.activeSessions.get(sessionId) || null;
247
+ }
248
+
249
+ /**
250
+ * Update session context
251
+ */
252
+ async updateSessionContext(
253
+ sessionId: string,
254
+ contextType: 'project' | 'command' | 'conversation',
255
+ contextData: any
256
+ ): Promise<void> {
257
+ const session = this.activeSessions.get(sessionId);
258
+ if (!session) {
259
+ throw new Error(`Session not found: ${sessionId}`);
260
+ }
261
+
262
+ // Add to context stack
263
+ session.contextStack.push({
264
+ type: contextType,
265
+ context: contextData,
266
+ timestamp: new Date(),
267
+ });
268
+
269
+ // Limit context stack size
270
+ if (session.contextStack.length > 50) {
271
+ session.contextStack = session.contextStack.slice(-25);
272
+ }
273
+
274
+ // Update specific context based on type
275
+ switch (contextType) {
276
+ case 'project':
277
+ session.projectContext = { ...session.projectContext, ...contextData };
278
+ break;
279
+ case 'conversation':
280
+ session.activeConversation = contextData.conversationId;
281
+ break;
282
+ }
283
+
284
+ session.lastAccessed = new Date();
285
+ await this.persistSession(session);
286
+
287
+ this.emit('context_updated', { sessionId, contextType, contextData });
288
+ }
289
+
290
+ /**
291
+ * Add command to session history
292
+ */
293
+ async addCommandToHistory(
294
+ sessionId: string,
295
+ command: string,
296
+ success: boolean,
297
+ duration: number
298
+ ): Promise<void> {
299
+ const session = this.activeSessions.get(sessionId);
300
+ if (!session) {
301
+ throw new Error(`Session not found: ${sessionId}`);
302
+ }
303
+
304
+ session.recentCommands.push({
305
+ command,
306
+ timestamp: new Date(),
307
+ success,
308
+ duration,
309
+ });
310
+
311
+ // Limit history size
312
+ const maxHistory = session.userPreferences.maxHistoryLength || 100;
313
+ if (session.recentCommands.length > maxHistory) {
314
+ session.recentCommands = session.recentCommands.slice(-maxHistory / 2);
315
+ }
316
+
317
+ session.lastAccessed = new Date();
318
+ await this.persistSession(session);
319
+
320
+ // Learn from command patterns if enabled
321
+ if (this.config.enableContextLearning) {
322
+ await this.analyzeCommandPatterns(session);
323
+ }
324
+
325
+ this.emit('command_recorded', { sessionId, command, success, duration });
326
+ }
327
+
328
+ /**
329
+ * Update user preferences
330
+ */
331
+ async updateUserPreferences(
332
+ userId: string,
333
+ preferences: Partial<UserPreferences>
334
+ ): Promise<void> {
335
+ const defaultPreferences = this.getDefaultPreferences();
336
+ const currentPreferences = await this.loadUserPreferences(userId);
337
+ const updatedPreferences = { ...currentPreferences, ...preferences };
338
+
339
+ await this.saveUserPreferences(userId, updatedPreferences);
340
+
341
+ // Update all active sessions for this user
342
+ for (const [sessionId, session] of this.activeSessions) {
343
+ if (session.userId === userId) {
344
+ session.userPreferences = updatedPreferences;
345
+ await this.persistSession(session);
346
+ }
347
+ }
348
+
349
+ this.emit('preferences_updated', {
350
+ userId,
351
+ preferences: updatedPreferences,
352
+ });
353
+ logger.debug(`Updated preferences for user: ${userId}`);
354
+ }
355
+
356
+ /**
357
+ * Register a workspace
358
+ */
359
+ async registerWorkspace(
360
+ workspacePath: string,
361
+ config: Partial<WorkspaceConfig>
362
+ ): Promise<void> {
363
+ const workspace: WorkspaceConfig = {
364
+ name: config.name || path.basename(workspacePath),
365
+ path: workspacePath,
366
+ type: config.type || 'unknown',
367
+ settings: config.settings || {},
368
+ lastAccessed: new Date(),
369
+ bookmarked: config.bookmarked || false,
370
+ };
371
+
372
+ this.workspaces.set(workspacePath, workspace);
373
+ await this.saveWorkspace(workspace);
374
+
375
+ this.emit('workspace_registered', { workspace });
376
+ logger.debug(`Registered workspace: ${workspacePath}`);
377
+ }
378
+
379
+ /**
380
+ * Get workspace suggestions based on recent activity
381
+ */
382
+ getWorkspaceSuggestions(limit: number = 5): WorkspaceConfig[] {
383
+ const workspaces = Array.from(this.workspaces.values()).sort(
384
+ (a, b) => b.lastAccessed.getTime() - a.lastAccessed.getTime()
385
+ );
386
+
387
+ return workspaces.slice(0, limit);
388
+ }
389
+
390
+ /**
391
+ * Search session history
392
+ */
393
+ searchHistory(
394
+ sessionId: string,
395
+ query: {
396
+ command?: string;
397
+ timeRange?: { from: Date; to: Date };
398
+ successOnly?: boolean;
399
+ limit?: number;
400
+ }
401
+ ): Array<{
402
+ command: string;
403
+ timestamp: Date;
404
+ success: boolean;
405
+ duration: number;
406
+ }> {
407
+ const session = this.activeSessions.get(sessionId);
408
+ if (!session) return [];
409
+
410
+ let history = session.recentCommands;
411
+
412
+ if (query.command) {
413
+ const searchTerm = query.command.toLowerCase();
414
+ history = history.filter(cmd =>
415
+ cmd.command.toLowerCase().includes(searchTerm)
416
+ );
417
+ }
418
+
419
+ if (query.timeRange) {
420
+ history = history.filter(
421
+ cmd =>
422
+ cmd.timestamp >= query.timeRange!.from &&
423
+ cmd.timestamp <= query.timeRange!.to
424
+ );
425
+ }
426
+
427
+ if (query.successOnly) {
428
+ history = history.filter(cmd => cmd.success);
429
+ }
430
+
431
+ if (query.limit) {
432
+ history = history.slice(-query.limit);
433
+ }
434
+
435
+ return history;
436
+ }
437
+
438
+ /**
439
+ * Get contextual suggestions based on current session
440
+ */
441
+ async getContextualSuggestions(
442
+ sessionId: string,
443
+ limit: number = 5
444
+ ): Promise<
445
+ Array<{
446
+ type: 'command' | 'workspace' | 'conversation';
447
+ suggestion: string;
448
+ description: string;
449
+ confidence: number;
450
+ }>
451
+ > {
452
+ const session = this.activeSessions.get(sessionId);
453
+ if (!session) return [];
454
+
455
+ const suggestions: Array<{
456
+ type: 'command' | 'workspace' | 'conversation';
457
+ suggestion: string;
458
+ description: string;
459
+ confidence: number;
460
+ }> = [];
461
+
462
+ // Command suggestions based on recent history
463
+ const recentCommands = session.recentCommands
464
+ .filter(cmd => cmd.success)
465
+ .slice(-10);
466
+
467
+ const commandFrequency = new Map<string, number>();
468
+ for (const cmd of recentCommands) {
469
+ const baseCommand = cmd.command.split(' ')[0];
470
+ if (baseCommand) {
471
+ commandFrequency.set(
472
+ baseCommand,
473
+ (commandFrequency.get(baseCommand) || 0) + 1
474
+ );
475
+ }
476
+ }
477
+
478
+ for (const [command, frequency] of commandFrequency) {
479
+ suggestions.push({
480
+ type: 'command',
481
+ suggestion: command,
482
+ description: `Recently used command (${frequency} times)`,
483
+ confidence: Math.min(0.9, frequency * 0.2),
484
+ });
485
+ }
486
+
487
+ // Workspace suggestions
488
+ const workspaceSuggestions = this.getWorkspaceSuggestions(3);
489
+ for (const workspace of workspaceSuggestions) {
490
+ if (workspace.path !== session.currentWorkspace) {
491
+ suggestions.push({
492
+ type: 'workspace',
493
+ suggestion: workspace.path,
494
+ description: `Switch to ${workspace.name}`,
495
+ confidence: 0.6,
496
+ });
497
+ }
498
+ }
499
+
500
+ // Sort by confidence and limit
501
+ suggestions.sort((a, b) => b.confidence - a.confidence);
502
+ return suggestions.slice(0, limit);
503
+ }
504
+
505
+ /**
506
+ * Export session data
507
+ */
508
+ async exportSession(
509
+ sessionId: string,
510
+ format: 'json' | 'csv'
511
+ ): Promise<string> {
512
+ const session = this.activeSessions.get(sessionId);
513
+ if (!session) {
514
+ throw new Error(`Session not found: ${sessionId}`);
515
+ }
516
+
517
+ switch (format) {
518
+ case 'json':
519
+ return JSON.stringify(session, null, 2);
520
+ case 'csv':
521
+ return this.sessionToCsv(session);
522
+ default:
523
+ throw new Error(`Unsupported export format: ${format}`);
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Clean up expired sessions
529
+ */
530
+ async cleanupSessions(): Promise<void> {
531
+ const now = new Date();
532
+ const timeoutMs = this.config.sessionTimeout * 60 * 1000;
533
+ const expiredSessions: string[] = [];
534
+
535
+ for (const [sessionId, session] of this.activeSessions) {
536
+ const timeSinceAccess = now.getTime() - session.lastAccessed.getTime();
537
+
538
+ if (timeSinceAccess > timeoutMs) {
539
+ expiredSessions.push(sessionId);
540
+ }
541
+ }
542
+
543
+ for (const sessionId of expiredSessions) {
544
+ const session = this.activeSessions.get(sessionId)!;
545
+
546
+ // Final persist before cleanup
547
+ await this.persistSession(session);
548
+
549
+ this.activeSessions.delete(sessionId);
550
+ this.emit('session_expired', { sessionId });
551
+ }
552
+
553
+ // Limit active sessions
554
+ if (this.activeSessions.size > this.config.maxSessions) {
555
+ const sessionsByAccess = Array.from(this.activeSessions.entries()).sort(
556
+ ([, a], [, b]) => a.lastAccessed.getTime() - b.lastAccessed.getTime()
557
+ );
558
+
559
+ const excessCount = this.activeSessions.size - this.config.maxSessions;
560
+ const toRemove = sessionsByAccess.slice(0, excessCount);
561
+
562
+ for (const [sessionId, session] of toRemove) {
563
+ await this.persistSession(session);
564
+ this.activeSessions.delete(sessionId);
565
+ this.emit('session_archived', { sessionId });
566
+ }
567
+ }
568
+
569
+ if (expiredSessions.length > 0) {
570
+ logger.debug(`Cleaned up ${expiredSessions.length} expired sessions`);
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Get session statistics
576
+ */
577
+ getSessionStats(): {
578
+ activeSessions: number;
579
+ totalCommands: number;
580
+ averageSessionDuration: number;
581
+ mostUsedCommands: Array<{ command: string; count: number }>;
582
+ workspaceCount: number;
583
+ } {
584
+ const activeSessions = this.activeSessions.size;
585
+ const totalCommands = Array.from(this.activeSessions.values()).reduce(
586
+ (sum, session) => sum + session.recentCommands.length,
587
+ 0
588
+ );
589
+
590
+ const sessionDurations = Array.from(this.activeSessions.values()).map(
591
+ session => session.lastAccessed.getTime() - session.created.getTime()
592
+ );
593
+ const averageSessionDuration =
594
+ sessionDurations.length > 0
595
+ ? sessionDurations.reduce((sum, duration) => sum + duration, 0) /
596
+ sessionDurations.length
597
+ : 0;
598
+
599
+ // Command frequency analysis
600
+ const commandCounts = new Map<string, number>();
601
+ for (const session of this.activeSessions.values()) {
602
+ for (const cmd of session.recentCommands) {
603
+ const baseCommand = cmd.command.split(' ')[0];
604
+ if (baseCommand) {
605
+ commandCounts.set(
606
+ baseCommand,
607
+ (commandCounts.get(baseCommand) || 0) + 1
608
+ );
609
+ }
610
+ }
611
+ }
612
+
613
+ const mostUsedCommands = Array.from(commandCounts.entries())
614
+ .map(([command, count]) => ({ command, count }))
615
+ .sort((a, b) => b.count - a.count)
616
+ .slice(0, 10);
617
+
618
+ return {
619
+ activeSessions,
620
+ totalCommands,
621
+ averageSessionDuration,
622
+ mostUsedCommands,
623
+ workspaceCount: this.workspaces.size,
624
+ };
625
+ }
626
+
627
+ // Private methods
628
+
629
+ private async initialize(): Promise<void> {
630
+ // Ensure directories exist
631
+ await fs.ensureDir(this.config.persistencePath);
632
+ await fs.ensureDir(path.join(this.config.persistencePath, 'users'));
633
+ await fs.ensureDir(path.join(this.config.persistencePath, 'workspaces'));
634
+
635
+ // Load existing workspaces
636
+ await this.loadAllWorkspaces();
637
+
638
+ // Start background tasks
639
+ this.startBackgroundTasks();
640
+
641
+ logger.debug('Session manager initialized');
642
+ }
643
+
644
+ private startBackgroundTasks(): void {
645
+ // Periodic backup
646
+ this.backupTimer = setInterval(
647
+ async () => {
648
+ try {
649
+ await this.backupActiveSessions();
650
+ } catch (error) {
651
+ logger.error('Session backup failed:', error);
652
+ }
653
+ },
654
+ this.config.backupInterval * 60 * 1000
655
+ );
656
+
657
+ // Periodic cleanup
658
+ this.cleanupTimer = setInterval(
659
+ async () => {
660
+ try {
661
+ await this.cleanupSessions();
662
+ } catch (error) {
663
+ logger.error('Session cleanup failed:', error);
664
+ }
665
+ },
666
+ 30 * 60 * 1000
667
+ ); // Every 30 minutes
668
+ }
669
+
670
+ private async detectProjectContext(
671
+ workspacePath: string
672
+ ): Promise<ProjectContext | undefined> {
673
+ if (!this.config.autoDetectProjects) return undefined;
674
+
675
+ try {
676
+ const context: ProjectContext = {
677
+ rootPath: workspacePath,
678
+ projectType: 'unknown',
679
+ packageManager: 'unknown',
680
+ dependencies: [],
681
+ devDependencies: [],
682
+ scripts: {},
683
+ };
684
+
685
+ // Detect project type and package manager
686
+ const packageJsonPath = path.join(workspacePath, 'package.json');
687
+ if (await fs.pathExists(packageJsonPath)) {
688
+ const packageJson = await fs.readJson(packageJsonPath);
689
+ context.dependencies = Object.keys(packageJson.dependencies || {});
690
+ context.devDependencies = Object.keys(
691
+ packageJson.devDependencies || {}
692
+ );
693
+ context.scripts = packageJson.scripts || {};
694
+
695
+ // Detect project type from dependencies
696
+ if (context.dependencies.includes('react')) {
697
+ context.projectType = 'react';
698
+ } else if (context.dependencies.includes('vue')) {
699
+ context.projectType = 'vue';
700
+ } else if (context.dependencies.includes('@angular/core')) {
701
+ context.projectType = 'angular';
702
+ } else {
703
+ context.projectType = 'nodejs';
704
+ }
705
+
706
+ // Detect package manager
707
+ if (await fs.pathExists(path.join(workspacePath, 'pnpm-lock.yaml'))) {
708
+ context.packageManager = 'pnpm';
709
+ } else if (await fs.pathExists(path.join(workspacePath, 'yarn.lock'))) {
710
+ context.packageManager = 'yarn';
711
+ } else if (
712
+ await fs.pathExists(path.join(workspacePath, 'package-lock.json'))
713
+ ) {
714
+ context.packageManager = 'npm';
715
+ }
716
+ }
717
+
718
+ // Detect other project types
719
+ if (await fs.pathExists(path.join(workspacePath, 'requirements.txt'))) {
720
+ context.projectType = 'python';
721
+ context.packageManager = 'pip';
722
+ } else if (await fs.pathExists(path.join(workspacePath, 'pom.xml'))) {
723
+ context.projectType = 'java';
724
+ context.packageManager = 'maven';
725
+ } else if (
726
+ await fs.pathExists(path.join(workspacePath, 'build.gradle'))
727
+ ) {
728
+ context.projectType = 'java';
729
+ context.packageManager = 'gradle';
730
+ }
731
+
732
+ // Git information
733
+ const gitPath = path.join(workspacePath, '.git');
734
+ if (await fs.pathExists(gitPath)) {
735
+ context.gitRepository = await this.getGitInfo(workspacePath);
736
+ }
737
+
738
+ return context;
739
+ } catch (error) {
740
+ logger.debug('Failed to detect project context:', error);
741
+ return undefined;
742
+ }
743
+ }
744
+
745
+ private async getGitInfo(
746
+ workspacePath: string
747
+ ): Promise<ProjectContext['gitRepository']> {
748
+ // Simplified git info extraction
749
+ return {
750
+ remote: 'origin',
751
+ branch: 'main',
752
+ status: 'clean',
753
+ hasUncommittedChanges: false,
754
+ };
755
+ }
756
+
757
+ private getDefaultPreferences(): UserPreferences {
758
+ return {
759
+ defaultModel: 'claude-3-5-sonnet-20241022',
760
+ preferredOutputFormat: 'table',
761
+ confirmDestructiveCommands: true,
762
+ enableStreamingResponses: true,
763
+ maxHistoryLength: 100,
764
+ autoSave: true,
765
+ theme: 'auto',
766
+ verbosity: 'normal',
767
+ aliases: {},
768
+ favorites: [],
769
+ workspacePreferences: {},
770
+ };
771
+ }
772
+
773
+ private async loadUserPreferences(userId: string): Promise<UserPreferences> {
774
+ const prefsPath = path.join(
775
+ this.config.persistencePath,
776
+ 'users',
777
+ `${userId}.json`
778
+ );
779
+
780
+ if (await fs.pathExists(prefsPath)) {
781
+ try {
782
+ const saved = await fs.readJson(prefsPath);
783
+ return { ...this.getDefaultPreferences(), ...saved };
784
+ } catch (error) {
785
+ logger.warn(`Failed to load preferences for ${userId}:`, error);
786
+ }
787
+ }
788
+
789
+ return this.getDefaultPreferences();
790
+ }
791
+
792
+ private async saveUserPreferences(
793
+ userId: string,
794
+ preferences: UserPreferences
795
+ ): Promise<void> {
796
+ const prefsPath = path.join(
797
+ this.config.persistencePath,
798
+ 'users',
799
+ `${userId}.json`
800
+ );
801
+ await fs.ensureDir(path.dirname(prefsPath));
802
+ await fs.writeJson(prefsPath, preferences, { spaces: 2 });
803
+ }
804
+
805
+ private async persistSession(session: SessionState): Promise<void> {
806
+ const sessionPath = path.join(
807
+ this.config.persistencePath,
808
+ `${session.id}.json`
809
+ );
810
+ const serialized = this.serializeSession(session);
811
+ await fs.writeJson(sessionPath, serialized, { spaces: 2 });
812
+ }
813
+
814
+ private async loadSession(sessionId: string): Promise<SessionState | null> {
815
+ const sessionPath = path.join(
816
+ this.config.persistencePath,
817
+ `${sessionId}.json`
818
+ );
819
+
820
+ if (await fs.pathExists(sessionPath)) {
821
+ try {
822
+ const data = await fs.readJson(sessionPath);
823
+ return this.deserializeSession(data);
824
+ } catch (error) {
825
+ logger.warn(`Failed to load session ${sessionId}:`, error);
826
+ }
827
+ }
828
+
829
+ return null;
830
+ }
831
+
832
+ private serializeSession(session: SessionState): any {
833
+ return {
834
+ ...session,
835
+ created: session.created.toISOString(),
836
+ lastAccessed: session.lastAccessed.toISOString(),
837
+ recentCommands: session.recentCommands.map(cmd => ({
838
+ ...cmd,
839
+ timestamp: cmd.timestamp.toISOString(),
840
+ })),
841
+ contextStack: session.contextStack.map(ctx => ({
842
+ ...ctx,
843
+ timestamp: ctx.timestamp.toISOString(),
844
+ })),
845
+ projectContext: session.projectContext
846
+ ? {
847
+ ...session.projectContext,
848
+ lastAnalysis: session.projectContext.lastAnalysis
849
+ ? {
850
+ ...session.projectContext.lastAnalysis,
851
+ timestamp:
852
+ session.projectContext.lastAnalysis.timestamp.toISOString(),
853
+ }
854
+ : undefined,
855
+ }
856
+ : undefined,
857
+ };
858
+ }
859
+
860
+ private deserializeSession(data: any): SessionState {
861
+ return {
862
+ ...data,
863
+ created: new Date(data.created),
864
+ lastAccessed: new Date(data.lastAccessed),
865
+ recentCommands: data.recentCommands.map((cmd: any) => ({
866
+ ...cmd,
867
+ timestamp: new Date(cmd.timestamp),
868
+ })),
869
+ contextStack: data.contextStack.map((ctx: any) => ({
870
+ ...ctx,
871
+ timestamp: new Date(ctx.timestamp),
872
+ })),
873
+ projectContext: data.projectContext
874
+ ? {
875
+ ...data.projectContext,
876
+ lastAnalysis: data.projectContext.lastAnalysis
877
+ ? {
878
+ ...data.projectContext.lastAnalysis,
879
+ timestamp: new Date(
880
+ data.projectContext.lastAnalysis.timestamp
881
+ ),
882
+ }
883
+ : undefined,
884
+ }
885
+ : undefined,
886
+ };
887
+ }
888
+
889
+ private async loadWorkspace(
890
+ workspacePath: string
891
+ ): Promise<WorkspaceConfig | null> {
892
+ const workspacesDir = path.join(this.config.persistencePath, 'workspaces');
893
+ const workspaceFile = path.join(
894
+ workspacesDir,
895
+ `${Buffer.from(workspacePath).toString('base64')}.json`
896
+ );
897
+
898
+ if (await fs.pathExists(workspaceFile)) {
899
+ try {
900
+ const data = await fs.readJson(workspaceFile);
901
+ data.lastAccessed = new Date(data.lastAccessed);
902
+ return data;
903
+ } catch (error) {
904
+ logger.warn(`Failed to load workspace ${workspacePath}:`, error);
905
+ }
906
+ }
907
+
908
+ return null;
909
+ }
910
+
911
+ private async saveWorkspace(workspace: WorkspaceConfig): Promise<void> {
912
+ const workspacesDir = path.join(this.config.persistencePath, 'workspaces');
913
+ const workspaceFile = path.join(
914
+ workspacesDir,
915
+ `${Buffer.from(workspace.path).toString('base64')}.json`
916
+ );
917
+
918
+ await fs.ensureDir(workspacesDir);
919
+ await fs.writeJson(
920
+ workspaceFile,
921
+ {
922
+ ...workspace,
923
+ lastAccessed: workspace.lastAccessed.toISOString(),
924
+ },
925
+ { spaces: 2 }
926
+ );
927
+ }
928
+
929
+ private async loadAllWorkspaces(): Promise<void> {
930
+ const workspacesDir = path.join(this.config.persistencePath, 'workspaces');
931
+
932
+ if (await fs.pathExists(workspacesDir)) {
933
+ const files = await fs.readdir(workspacesDir);
934
+
935
+ for (const file of files) {
936
+ if (file.endsWith('.json')) {
937
+ try {
938
+ const data = await fs.readJson(path.join(workspacesDir, file));
939
+ data.lastAccessed = new Date(data.lastAccessed);
940
+ this.workspaces.set(data.path, data);
941
+ } catch (error) {
942
+ logger.debug(`Failed to load workspace file ${file}:`, error);
943
+ }
944
+ }
945
+ }
946
+ }
947
+ }
948
+
949
+ private async backupActiveSessions(): Promise<void> {
950
+ for (const session of this.activeSessions.values()) {
951
+ await this.persistSession(session);
952
+ }
953
+
954
+ logger.debug(`Backed up ${this.activeSessions.size} active sessions`);
955
+ }
956
+
957
+ private async analyzeCommandPatterns(session: SessionState): Promise<void> {
958
+ // Simple pattern analysis for learning user behavior
959
+ const recentCommands = session.recentCommands.slice(-10);
960
+
961
+ if (recentCommands.length >= 3) {
962
+ // Look for command sequences
963
+ const sequences: string[] = [];
964
+ for (let i = 0; i < recentCommands.length - 1; i++) {
965
+ const current = recentCommands[i];
966
+ const next = recentCommands[i + 1];
967
+ if (current?.command && next?.command) {
968
+ sequences.push(`${current.command} -> ${next.command}`);
969
+ }
970
+ }
971
+
972
+ // Store patterns in temporary data for future suggestions
973
+ if (!session.temporaryData['commandPatterns']) {
974
+ session.temporaryData['commandPatterns'] = [];
975
+ }
976
+
977
+ session.temporaryData['commandPatterns'].push(...sequences);
978
+
979
+ // Keep only recent patterns
980
+ session.temporaryData['commandPatterns'] =
981
+ session.temporaryData['commandPatterns'].slice(-20);
982
+ }
983
+ }
984
+
985
+ private sessionToCsv(session: SessionState): string {
986
+ const headers = ['Timestamp', 'Command', 'Success', 'Duration'];
987
+ const rows = [headers];
988
+
989
+ session.recentCommands.forEach(cmd => {
990
+ rows.push([
991
+ cmd.timestamp.toISOString(),
992
+ `"${cmd.command.replace(/"/g, '""')}"`,
993
+ cmd.success.toString(),
994
+ cmd.duration.toString(),
995
+ ]);
996
+ });
997
+
998
+ return rows.map(row => row.join(',')).join('\n');
999
+ }
1000
+
1001
+ /**
1002
+ * Cleanup resources
1003
+ */
1004
+ destroy(): void {
1005
+ if (this.backupTimer) {
1006
+ clearInterval(this.backupTimer);
1007
+ }
1008
+
1009
+ if (this.cleanupTimer) {
1010
+ clearInterval(this.cleanupTimer);
1011
+ }
1012
+
1013
+ this.activeSessions.clear();
1014
+ this.workspaces.clear();
1015
+ this.removeAllListeners();
1016
+ }
1017
+ }
1018
+
1019
+ export default SessionManager;