@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,907 @@
1
+ import { EventEmitter } from 'events';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { ClaudeClient, ClaudeMessage } from './claude-client';
5
+ import { ChatSession, ChatMessage } from '../types';
6
+ import { logger } from '../utils/logger';
7
+
8
+ /**
9
+ * Configuration for conversation management
10
+ */
11
+ export interface ConversationConfig {
12
+ maxHistoryLength: number;
13
+ contextWindowSize: number;
14
+ sessionTimeout: number; // minutes
15
+ persistencePath: string;
16
+ autoSave: boolean;
17
+ compressionThreshold: number;
18
+ }
19
+
20
+ /**
21
+ * Session metadata for enhanced tracking
22
+ */
23
+ export interface SessionMetadata {
24
+ tokenUsage: {
25
+ total: number;
26
+ prompt: number;
27
+ completion: number;
28
+ };
29
+ performance: {
30
+ averageResponseTime: number;
31
+ totalRequests: number;
32
+ errorCount: number;
33
+ };
34
+ context: {
35
+ projectPath?: string;
36
+ currentCommand?: string;
37
+ userPreferences: Record<string, any>;
38
+ workflow?: string[];
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Enhanced chat session with metadata
44
+ */
45
+ export interface EnhancedChatSession extends ChatSession {
46
+ metadata: SessionMetadata;
47
+ tags: string[];
48
+ archived: boolean;
49
+ lastAccessed: Date;
50
+ }
51
+
52
+ /**
53
+ * Conversation manager handles session persistence, context optimization, and memory management
54
+ */
55
+ export class ConversationManager extends EventEmitter {
56
+ private activeSessions: Map<string, EnhancedChatSession>;
57
+ private claudeClient: ClaudeClient;
58
+ private config: ConversationConfig;
59
+ private sessionCleanupTimer?: NodeJS.Timeout;
60
+
61
+ constructor(
62
+ claudeClient: ClaudeClient,
63
+ config: Partial<ConversationConfig> = {}
64
+ ) {
65
+ super();
66
+
67
+ this.claudeClient = claudeClient;
68
+ this.config = {
69
+ maxHistoryLength: 100,
70
+ contextWindowSize: 20,
71
+ sessionTimeout: 60, // 1 hour
72
+ persistencePath: path.join(process.cwd(), '.wundr', 'conversations'),
73
+ autoSave: true,
74
+ compressionThreshold: 50,
75
+ ...config,
76
+ };
77
+
78
+ this.activeSessions = new Map();
79
+
80
+ this.initializeManager();
81
+ }
82
+
83
+ /**
84
+ * Create a new conversation session
85
+ */
86
+ async createSession(
87
+ options: {
88
+ id?: string;
89
+ model?: string;
90
+ context?: string;
91
+ tags?: string[];
92
+ metadata?: Partial<SessionMetadata>;
93
+ } = {}
94
+ ): Promise<EnhancedChatSession> {
95
+ const sessionId =
96
+ options.id ||
97
+ `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
98
+
99
+ const session: EnhancedChatSession = {
100
+ id: sessionId,
101
+ model: options.model || this.claudeClient.getModelInfo().model,
102
+ context: options.context,
103
+ history: [],
104
+ created: new Date(),
105
+ updated: new Date(),
106
+ archived: false,
107
+ lastAccessed: new Date(),
108
+ tags: options.tags || [],
109
+ metadata: {
110
+ tokenUsage: { total: 0, prompt: 0, completion: 0 },
111
+ performance: {
112
+ averageResponseTime: 0,
113
+ totalRequests: 0,
114
+ errorCount: 0,
115
+ },
116
+ context: {
117
+ userPreferences: {},
118
+ ...options.metadata?.context,
119
+ },
120
+ ...options.metadata,
121
+ },
122
+ };
123
+
124
+ this.activeSessions.set(sessionId, session);
125
+
126
+ if (this.config.autoSave) {
127
+ await this.persistSession(session);
128
+ }
129
+
130
+ this.emit('session_created', { sessionId, session });
131
+ logger.debug(`Created conversation session: ${sessionId}`);
132
+
133
+ return session;
134
+ }
135
+
136
+ /**
137
+ * Load an existing session
138
+ */
139
+ async loadSession(sessionId: string): Promise<EnhancedChatSession | null> {
140
+ // Check active sessions first
141
+ if (this.activeSessions.has(sessionId)) {
142
+ const session = this.activeSessions.get(sessionId)!;
143
+ session.lastAccessed = new Date();
144
+ return session;
145
+ }
146
+
147
+ // Load from persistence
148
+ try {
149
+ const sessionPath = this.getSessionPath(sessionId);
150
+ if (await fs.pathExists(sessionPath)) {
151
+ const data = await fs.readJson(sessionPath);
152
+ const session = this.deserializeSession(data);
153
+
154
+ session.lastAccessed = new Date();
155
+ this.activeSessions.set(sessionId, session);
156
+
157
+ this.emit('session_loaded', { sessionId, session });
158
+ logger.debug(`Loaded conversation session: ${sessionId}`);
159
+
160
+ return session;
161
+ }
162
+ } catch (error) {
163
+ logger.error(`Failed to load session ${sessionId}:`, error);
164
+ }
165
+
166
+ return null;
167
+ }
168
+
169
+ /**
170
+ * Send a message in a conversation session
171
+ */
172
+ async sendMessage(
173
+ sessionId: string,
174
+ message: string,
175
+ options: {
176
+ systemPrompt?: string;
177
+ streaming?: boolean;
178
+ metadata?: Record<string, any>;
179
+ } = {}
180
+ ): Promise<string | AsyncGenerator<string, void, unknown>> {
181
+ const session = await this.loadSession(sessionId);
182
+ if (!session) {
183
+ throw new Error(`Session not found: ${sessionId}`);
184
+ }
185
+
186
+ const startTime = Date.now();
187
+
188
+ try {
189
+ // Add user message to history
190
+ const userMessage: ChatMessage = {
191
+ role: 'user',
192
+ content: message,
193
+ timestamp: new Date(),
194
+ metadata: options.metadata,
195
+ };
196
+
197
+ session.history.push(userMessage);
198
+
199
+ // Optimize conversation context
200
+ const contextMessages = this.optimizeContext(session);
201
+
202
+ // Convert to Claude message format
203
+ const claudeMessages: ClaudeMessage[] = contextMessages.map(msg => ({
204
+ role: msg.role,
205
+ content: msg.content,
206
+ }));
207
+
208
+ // Send to Claude
209
+ let response: string | AsyncGenerator<string, void, unknown>;
210
+
211
+ if (options.streaming) {
212
+ response = this.claudeClient.streamConversation(
213
+ claudeMessages,
214
+ options.systemPrompt
215
+ );
216
+ } else {
217
+ response = await this.claudeClient.sendConversation(
218
+ claudeMessages,
219
+ options.systemPrompt
220
+ );
221
+ }
222
+
223
+ // Handle streaming response
224
+ if (options.streaming) {
225
+ return this.handleStreamingResponse(
226
+ session,
227
+ response as AsyncGenerator<string, void, unknown>,
228
+ startTime
229
+ );
230
+ } else {
231
+ return await this.handleSyncResponse(
232
+ session,
233
+ response as string,
234
+ startTime
235
+ );
236
+ }
237
+ } catch (error) {
238
+ session.metadata.performance.errorCount++;
239
+ this.emit('message_error', { sessionId, error, message });
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Get conversation history with optional filtering
246
+ */
247
+ getHistory(
248
+ sessionId: string,
249
+ options: {
250
+ limit?: number;
251
+ from?: Date;
252
+ to?: Date;
253
+ roles?: ('user' | 'assistant' | 'system')[];
254
+ } = {}
255
+ ): ChatMessage[] {
256
+ const session = this.activeSessions.get(sessionId);
257
+ if (!session) {
258
+ return [];
259
+ }
260
+
261
+ let history = [...session.history];
262
+
263
+ // Apply filters
264
+ if (options.from) {
265
+ history = history.filter(msg => msg.timestamp >= options.from!);
266
+ }
267
+
268
+ if (options.to) {
269
+ history = history.filter(msg => msg.timestamp <= options.to!);
270
+ }
271
+
272
+ if (options.roles) {
273
+ history = history.filter(msg => options.roles!.includes(msg.role));
274
+ }
275
+
276
+ if (options.limit) {
277
+ history = history.slice(-options.limit);
278
+ }
279
+
280
+ return history;
281
+ }
282
+
283
+ /**
284
+ * Add context to a conversation session
285
+ */
286
+ async addContext(
287
+ sessionId: string,
288
+ contextType: 'project' | 'command' | 'preference' | 'workflow',
289
+ contextData: any
290
+ ): Promise<void> {
291
+ const session = await this.loadSession(sessionId);
292
+ if (!session) {
293
+ throw new Error(`Session not found: ${sessionId}`);
294
+ }
295
+
296
+ switch (contextType) {
297
+ case 'project':
298
+ session.metadata.context.projectPath = contextData.path;
299
+ break;
300
+ case 'command':
301
+ session.metadata.context.currentCommand = contextData.command;
302
+ break;
303
+ case 'preference':
304
+ Object.assign(session.metadata.context.userPreferences, contextData);
305
+ break;
306
+ case 'workflow':
307
+ if (!session.metadata.context.workflow) {
308
+ session.metadata.context.workflow = [];
309
+ }
310
+ session.metadata.context.workflow.push(...contextData);
311
+ break;
312
+ }
313
+
314
+ session.updated = new Date();
315
+
316
+ if (this.config.autoSave) {
317
+ await this.persistSession(session);
318
+ }
319
+
320
+ this.emit('context_updated', { sessionId, contextType, contextData });
321
+ }
322
+
323
+ /**
324
+ * Archive old or inactive sessions
325
+ */
326
+ async archiveSessions(
327
+ criteria: {
328
+ olderThan?: number; // days
329
+ inactiveFor?: number; // days
330
+ maxSessions?: number;
331
+ } = {}
332
+ ): Promise<string[]> {
333
+ const archivedSessions: string[] = [];
334
+ const now = new Date();
335
+
336
+ for (const [sessionId, session] of this.activeSessions) {
337
+ let shouldArchive = false;
338
+
339
+ if (criteria.olderThan) {
340
+ const ageInDays =
341
+ (now.getTime() - session.created.getTime()) / (1000 * 60 * 60 * 24);
342
+ if (ageInDays > criteria.olderThan) {
343
+ shouldArchive = true;
344
+ }
345
+ }
346
+
347
+ if (criteria.inactiveFor) {
348
+ const inactiveInDays =
349
+ (now.getTime() - session.lastAccessed.getTime()) /
350
+ (1000 * 60 * 60 * 24);
351
+ if (inactiveInDays > criteria.inactiveFor) {
352
+ shouldArchive = true;
353
+ }
354
+ }
355
+
356
+ if (shouldArchive) {
357
+ session.archived = true;
358
+ await this.persistSession(session);
359
+ this.activeSessions.delete(sessionId);
360
+ archivedSessions.push(sessionId);
361
+ }
362
+ }
363
+
364
+ // Handle maxSessions limit
365
+ if (
366
+ criteria.maxSessions &&
367
+ this.activeSessions.size > criteria.maxSessions
368
+ ) {
369
+ const sessionsByAccess = Array.from(this.activeSessions.entries()).sort(
370
+ ([, a], [, b]) => a.lastAccessed.getTime() - b.lastAccessed.getTime()
371
+ );
372
+
373
+ const excessCount = this.activeSessions.size - criteria.maxSessions;
374
+ const toArchive = sessionsByAccess.slice(0, excessCount);
375
+
376
+ for (const [sessionId, session] of toArchive) {
377
+ session.archived = true;
378
+ await this.persistSession(session);
379
+ this.activeSessions.delete(sessionId);
380
+ archivedSessions.push(sessionId);
381
+ }
382
+ }
383
+
384
+ this.emit('sessions_archived', {
385
+ count: archivedSessions.length,
386
+ sessionIds: archivedSessions,
387
+ });
388
+ logger.debug(`Archived ${archivedSessions.length} sessions`);
389
+
390
+ return archivedSessions;
391
+ }
392
+
393
+ /**
394
+ * Search conversations by content or metadata
395
+ */
396
+ async searchConversations(query: {
397
+ text?: string;
398
+ tags?: string[];
399
+ dateRange?: { from: Date; to: Date };
400
+ model?: string;
401
+ limit?: number;
402
+ }): Promise<
403
+ Array<{ sessionId: string; matches: ChatMessage[]; score: number }>
404
+ > {
405
+ const results: Array<{
406
+ sessionId: string;
407
+ matches: ChatMessage[];
408
+ score: number;
409
+ }> = [];
410
+
411
+ // Search active sessions
412
+ for (const [sessionId, session] of this.activeSessions) {
413
+ const matches = this.searchSession(session, query);
414
+ if (matches.length > 0) {
415
+ results.push({
416
+ sessionId,
417
+ matches,
418
+ score: this.calculateSearchScore(matches, query),
419
+ });
420
+ }
421
+ }
422
+
423
+ // Search persisted sessions if needed
424
+ // This could be expensive, so implement pagination/indexing for production
425
+
426
+ // Sort by score and apply limit
427
+ results.sort((a, b) => b.score - a.score);
428
+
429
+ if (query.limit) {
430
+ results.splice(query.limit);
431
+ }
432
+
433
+ return results;
434
+ }
435
+
436
+ /**
437
+ * Get session statistics
438
+ */
439
+ getSessionStats(sessionId?: string): any {
440
+ if (sessionId) {
441
+ const session = this.activeSessions.get(sessionId);
442
+ if (!session) return null;
443
+
444
+ return {
445
+ messageCount: session.history.length,
446
+ tokenUsage: session.metadata.tokenUsage,
447
+ performance: session.metadata.performance,
448
+ created: session.created,
449
+ lastAccessed: session.lastAccessed,
450
+ tags: session.tags,
451
+ };
452
+ }
453
+
454
+ // Global stats
455
+ const totalSessions = this.activeSessions.size;
456
+ const totalMessages = Array.from(this.activeSessions.values()).reduce(
457
+ (sum, session) => sum + session.history.length,
458
+ 0
459
+ );
460
+
461
+ const totalTokens = Array.from(this.activeSessions.values()).reduce(
462
+ (sum, session) => sum + session.metadata.tokenUsage.total,
463
+ 0
464
+ );
465
+
466
+ return {
467
+ totalSessions,
468
+ totalMessages,
469
+ totalTokens,
470
+ averageMessagesPerSession:
471
+ totalSessions > 0 ? totalMessages / totalSessions : 0,
472
+ };
473
+ }
474
+
475
+ /**
476
+ * Clean up expired sessions
477
+ */
478
+ async cleanup(): Promise<void> {
479
+ const expiredSessionIds: string[] = [];
480
+ const now = new Date();
481
+ const timeoutMs = this.config.sessionTimeout * 60 * 1000;
482
+
483
+ for (const [sessionId, session] of this.activeSessions) {
484
+ const timeSinceAccess = now.getTime() - session.lastAccessed.getTime();
485
+
486
+ if (timeSinceAccess > timeoutMs) {
487
+ expiredSessionIds.push(sessionId);
488
+ }
489
+ }
490
+
491
+ for (const sessionId of expiredSessionIds) {
492
+ const session = this.activeSessions.get(sessionId)!;
493
+
494
+ // Persist before removing
495
+ if (this.config.autoSave) {
496
+ await this.persistSession(session);
497
+ }
498
+
499
+ this.activeSessions.delete(sessionId);
500
+ this.emit('session_expired', { sessionId });
501
+ }
502
+
503
+ if (expiredSessionIds.length > 0) {
504
+ logger.debug(`Cleaned up ${expiredSessionIds.length} expired sessions`);
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Export conversation data
510
+ */
511
+ async exportSession(
512
+ sessionId: string,
513
+ format: 'json' | 'markdown' | 'csv'
514
+ ): Promise<string> {
515
+ const session = await this.loadSession(sessionId);
516
+ if (!session) {
517
+ throw new Error(`Session not found: ${sessionId}`);
518
+ }
519
+
520
+ switch (format) {
521
+ case 'json':
522
+ return JSON.stringify(session, null, 2);
523
+ case 'markdown':
524
+ return this.sessionToMarkdown(session);
525
+ case 'csv':
526
+ return this.sessionToCsv(session);
527
+ default:
528
+ throw new Error(`Unsupported export format: ${format}`);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Import conversation data
534
+ */
535
+ async importSession(data: string, format: 'json'): Promise<string> {
536
+ if (format !== 'json') {
537
+ throw new Error(`Unsupported import format: ${format}`);
538
+ }
539
+
540
+ const sessionData = JSON.parse(data);
541
+ const session = this.deserializeSession(sessionData);
542
+
543
+ // Generate new ID to avoid conflicts
544
+ session.id = `imported-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
545
+ session.created = new Date();
546
+ session.lastAccessed = new Date();
547
+
548
+ this.activeSessions.set(session.id, session);
549
+
550
+ if (this.config.autoSave) {
551
+ await this.persistSession(session);
552
+ }
553
+
554
+ this.emit('session_imported', { sessionId: session.id });
555
+ return session.id;
556
+ }
557
+
558
+ /**
559
+ * List all sessions with pagination
560
+ */
561
+ async listSessions(
562
+ options: {
563
+ limit?: number;
564
+ offset?: number;
565
+ includeArchived?: boolean;
566
+ sortBy?: 'created' | 'updated' | 'lastAccessed';
567
+ sortOrder?: 'asc' | 'desc';
568
+ } = {}
569
+ ): Promise<{
570
+ sessions: Array<
571
+ Pick<
572
+ EnhancedChatSession,
573
+ 'id' | 'created' | 'updated' | 'lastAccessed' | 'tags' | 'archived'
574
+ >
575
+ >;
576
+ total: number;
577
+ hasMore: boolean;
578
+ }> {
579
+ const allSessions = Array.from(this.activeSessions.values());
580
+
581
+ // Filter archived if needed
582
+ const filteredSessions = options.includeArchived
583
+ ? allSessions
584
+ : allSessions.filter(s => !s.archived);
585
+
586
+ // Sort
587
+ const sortBy = options.sortBy || 'lastAccessed';
588
+ const sortOrder = options.sortOrder || 'desc';
589
+
590
+ filteredSessions.sort((a, b) => {
591
+ const aValue = a[sortBy].getTime();
592
+ const bValue = b[sortBy].getTime();
593
+ return sortOrder === 'desc' ? bValue - aValue : aValue - bValue;
594
+ });
595
+
596
+ // Paginate
597
+ const offset = options.offset || 0;
598
+ const limit = options.limit || 50;
599
+ const paginatedSessions = filteredSessions.slice(offset, offset + limit);
600
+
601
+ return {
602
+ sessions: paginatedSessions.map(s => ({
603
+ id: s.id,
604
+ created: s.created,
605
+ updated: s.updated,
606
+ lastAccessed: s.lastAccessed,
607
+ tags: s.tags,
608
+ archived: s.archived,
609
+ })),
610
+ total: filteredSessions.length,
611
+ hasMore: offset + limit < filteredSessions.length,
612
+ };
613
+ }
614
+
615
+ // Private helper methods
616
+
617
+ private async initializeManager(): Promise<void> {
618
+ // Ensure persistence directory exists
619
+ await fs.ensureDir(this.config.persistencePath);
620
+
621
+ // Start cleanup timer
622
+ this.sessionCleanupTimer = setInterval(
623
+ () => {
624
+ this.cleanup().catch(error => {
625
+ logger.error('Session cleanup failed:', error);
626
+ });
627
+ },
628
+ 10 * 60 * 1000
629
+ ); // Every 10 minutes
630
+
631
+ logger.debug('Conversation manager initialized');
632
+ }
633
+
634
+ private optimizeContext(session: EnhancedChatSession): ChatMessage[] {
635
+ const { contextWindowSize, compressionThreshold } = this.config;
636
+
637
+ if (session.history.length <= contextWindowSize) {
638
+ return session.history;
639
+ }
640
+
641
+ // Keep system messages and recent messages
642
+ const systemMessages = session.history.filter(msg => msg.role === 'system');
643
+ const recentMessages = session.history.slice(
644
+ -contextWindowSize + systemMessages.length
645
+ );
646
+
647
+ // If we still have too many messages, apply compression
648
+ if (recentMessages.length > compressionThreshold) {
649
+ return this.compressHistory([...systemMessages, ...recentMessages]);
650
+ }
651
+
652
+ return [...systemMessages, ...recentMessages];
653
+ }
654
+
655
+ private compressHistory(messages: ChatMessage[]): ChatMessage[] {
656
+ // Simple compression: summarize older messages
657
+ // In a production system, you might use Claude to generate summaries
658
+
659
+ const recentCount = 10;
660
+ const recent = messages.slice(-recentCount);
661
+ const older = messages.slice(0, -recentCount);
662
+
663
+ if (older.length === 0) return recent;
664
+
665
+ const summary: ChatMessage = {
666
+ role: 'system',
667
+ content: `[Previous conversation summary: ${older.length} messages covering topics and discussions that led to the current context]`,
668
+ timestamp: new Date(),
669
+ };
670
+
671
+ return [summary, ...recent];
672
+ }
673
+
674
+ private async handleSyncResponse(
675
+ session: EnhancedChatSession,
676
+ response: string,
677
+ startTime: number
678
+ ): Promise<string> {
679
+ const endTime = Date.now();
680
+ const responseTime = endTime - startTime;
681
+
682
+ // Add AI response to history
683
+ const aiMessage: ChatMessage = {
684
+ role: 'assistant',
685
+ content: response,
686
+ timestamp: new Date(),
687
+ };
688
+
689
+ session.history.push(aiMessage);
690
+ session.updated = new Date();
691
+ session.lastAccessed = new Date();
692
+
693
+ // Update performance metrics
694
+ this.updatePerformanceMetrics(session, responseTime);
695
+
696
+ if (this.config.autoSave) {
697
+ await this.persistSession(session);
698
+ }
699
+
700
+ this.emit('message_complete', {
701
+ sessionId: session.id,
702
+ responseTime,
703
+ tokenCount: response.length, // Rough approximation
704
+ });
705
+
706
+ return response;
707
+ }
708
+
709
+ private async *handleStreamingResponse(
710
+ session: EnhancedChatSession,
711
+ responseGenerator: AsyncGenerator<string, void, unknown>,
712
+ startTime: number
713
+ ): AsyncGenerator<string, void, unknown> {
714
+ let fullResponse = '';
715
+ let firstChunk = true;
716
+
717
+ for await (const chunk of responseGenerator) {
718
+ if (firstChunk) {
719
+ this.emit('stream_started', { sessionId: session.id });
720
+ firstChunk = false;
721
+ }
722
+
723
+ fullResponse += chunk;
724
+ yield chunk;
725
+ }
726
+
727
+ const endTime = Date.now();
728
+ const responseTime = endTime - startTime;
729
+
730
+ // Add complete response to history
731
+ const aiMessage: ChatMessage = {
732
+ role: 'assistant',
733
+ content: fullResponse,
734
+ timestamp: new Date(),
735
+ };
736
+
737
+ session.history.push(aiMessage);
738
+ session.updated = new Date();
739
+ session.lastAccessed = new Date();
740
+
741
+ this.updatePerformanceMetrics(session, responseTime);
742
+
743
+ if (this.config.autoSave) {
744
+ await this.persistSession(session);
745
+ }
746
+
747
+ this.emit('stream_complete', {
748
+ sessionId: session.id,
749
+ responseTime,
750
+ tokenCount: fullResponse.length,
751
+ });
752
+ }
753
+
754
+ private updatePerformanceMetrics(
755
+ session: EnhancedChatSession,
756
+ responseTime: number
757
+ ): void {
758
+ const perf = session.metadata.performance;
759
+ perf.totalRequests++;
760
+ perf.averageResponseTime =
761
+ (perf.averageResponseTime * (perf.totalRequests - 1) + responseTime) /
762
+ perf.totalRequests;
763
+ }
764
+
765
+ private async persistSession(session: EnhancedChatSession): Promise<void> {
766
+ const sessionPath = this.getSessionPath(session.id);
767
+ const serialized = this.serializeSession(session);
768
+
769
+ await fs.ensureDir(path.dirname(sessionPath));
770
+ await fs.writeJson(sessionPath, serialized, { spaces: 2 });
771
+ }
772
+
773
+ private getSessionPath(sessionId: string): string {
774
+ return path.join(this.config.persistencePath, `${sessionId}.json`);
775
+ }
776
+
777
+ private serializeSession(session: EnhancedChatSession): any {
778
+ return {
779
+ ...session,
780
+ created: session.created.toISOString(),
781
+ updated: session.updated.toISOString(),
782
+ lastAccessed: session.lastAccessed.toISOString(),
783
+ history: session.history.map(msg => ({
784
+ ...msg,
785
+ timestamp: msg.timestamp.toISOString(),
786
+ })),
787
+ };
788
+ }
789
+
790
+ private deserializeSession(data: any): EnhancedChatSession {
791
+ return {
792
+ ...data,
793
+ created: new Date(data.created),
794
+ updated: new Date(data.updated),
795
+ lastAccessed: new Date(data.lastAccessed),
796
+ history: data.history.map((msg: any) => ({
797
+ ...msg,
798
+ timestamp: new Date(msg.timestamp),
799
+ })),
800
+ };
801
+ }
802
+
803
+ private searchSession(
804
+ session: EnhancedChatSession,
805
+ query: any
806
+ ): ChatMessage[] {
807
+ let matches = session.history;
808
+
809
+ if (query.text) {
810
+ const searchText = query.text.toLowerCase();
811
+ matches = matches.filter(msg =>
812
+ msg.content.toLowerCase().includes(searchText)
813
+ );
814
+ }
815
+
816
+ if (query.dateRange) {
817
+ matches = matches.filter(
818
+ msg =>
819
+ msg.timestamp >= query.dateRange.from &&
820
+ msg.timestamp <= query.dateRange.to
821
+ );
822
+ }
823
+
824
+ if (query.tags && query.tags.length > 0) {
825
+ const hasMatchingTag = query.tags.some((tag: string) =>
826
+ session.tags.includes(tag)
827
+ );
828
+ if (!hasMatchingTag) {
829
+ matches = [];
830
+ }
831
+ }
832
+
833
+ if (query.model && session.model !== query.model) {
834
+ matches = [];
835
+ }
836
+
837
+ return matches;
838
+ }
839
+
840
+ private calculateSearchScore(matches: ChatMessage[], query: any): number {
841
+ let score = matches.length;
842
+
843
+ if (query.text) {
844
+ // Boost score based on exact matches
845
+ const exactMatches = matches.filter(msg =>
846
+ msg.content.toLowerCase().includes(query.text.toLowerCase())
847
+ ).length;
848
+ score += exactMatches * 2;
849
+ }
850
+
851
+ return score;
852
+ }
853
+
854
+ private sessionToMarkdown(session: EnhancedChatSession): string {
855
+ let markdown = `# Conversation: ${session.id}\n\n`;
856
+ markdown += `**Model:** ${session.model}\n`;
857
+ markdown += `**Created:** ${session.created.toLocaleString()}\n`;
858
+ markdown += `**Last Updated:** ${session.updated.toLocaleString()}\n`;
859
+ markdown += `**Messages:** ${session.history.length}\n`;
860
+
861
+ if (session.tags.length > 0) {
862
+ markdown += `**Tags:** ${session.tags.join(', ')}\n`;
863
+ }
864
+
865
+ markdown += '\n---\n\n';
866
+
867
+ session.history.forEach((msg, index) => {
868
+ const role = msg.role === 'user' ? '🧑 **You**' : '🤖 **Assistant**';
869
+ markdown += `## Message ${index + 1}\n\n`;
870
+ markdown += `${role} - *${msg.timestamp.toLocaleString()}*\n\n`;
871
+ markdown += `${msg.content}\n\n`;
872
+ markdown += '---\n\n';
873
+ });
874
+
875
+ return markdown;
876
+ }
877
+
878
+ private sessionToCsv(session: EnhancedChatSession): string {
879
+ const headers = ['Index', 'Role', 'Content', 'Timestamp'];
880
+ const rows = [headers];
881
+
882
+ session.history.forEach((msg, index) => {
883
+ rows.push([
884
+ (index + 1).toString(),
885
+ msg.role,
886
+ `"${msg.content.replace(/"/g, '""')}"`, // Escape quotes
887
+ msg.timestamp.toISOString(),
888
+ ]);
889
+ });
890
+
891
+ return rows.map(row => row.join(',')).join('\n');
892
+ }
893
+
894
+ /**
895
+ * Cleanup resources
896
+ */
897
+ destroy(): void {
898
+ if (this.sessionCleanupTimer) {
899
+ clearInterval(this.sessionCleanupTimer);
900
+ }
901
+
902
+ this.activeSessions.clear();
903
+ this.removeAllListeners();
904
+ }
905
+ }
906
+
907
+ export default ConversationManager;