mcp-rubber-duck 1.1.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 (184) hide show
  1. package/.dockerignore +19 -0
  2. package/.env.desktop.example +145 -0
  3. package/.env.example +45 -0
  4. package/.env.pi.example +106 -0
  5. package/.env.template +165 -0
  6. package/.eslintrc.json +40 -0
  7. package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
  9. package/.github/ISSUE_TEMPLATE/question.md +67 -0
  10. package/.github/pull_request_template.md +111 -0
  11. package/.github/workflows/docker-build.yml +138 -0
  12. package/.github/workflows/release.yml +182 -0
  13. package/.github/workflows/security.yml +141 -0
  14. package/.github/workflows/semantic-release.yml +89 -0
  15. package/.prettierrc +10 -0
  16. package/.releaserc.json +66 -0
  17. package/CHANGELOG.md +95 -0
  18. package/CONTRIBUTING.md +242 -0
  19. package/Dockerfile +62 -0
  20. package/LICENSE +21 -0
  21. package/README.md +803 -0
  22. package/audit-ci.json +8 -0
  23. package/config/claude_desktop.json +14 -0
  24. package/config/config.example.json +91 -0
  25. package/dist/config/config.d.ts +51 -0
  26. package/dist/config/config.d.ts.map +1 -0
  27. package/dist/config/config.js +301 -0
  28. package/dist/config/config.js.map +1 -0
  29. package/dist/config/types.d.ts +356 -0
  30. package/dist/config/types.d.ts.map +1 -0
  31. package/dist/config/types.js +41 -0
  32. package/dist/config/types.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +109 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/providers/duck-provider-enhanced.d.ts +29 -0
  38. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
  39. package/dist/providers/duck-provider-enhanced.js +230 -0
  40. package/dist/providers/duck-provider-enhanced.js.map +1 -0
  41. package/dist/providers/enhanced-manager.d.ts +54 -0
  42. package/dist/providers/enhanced-manager.d.ts.map +1 -0
  43. package/dist/providers/enhanced-manager.js +217 -0
  44. package/dist/providers/enhanced-manager.js.map +1 -0
  45. package/dist/providers/manager.d.ts +28 -0
  46. package/dist/providers/manager.d.ts.map +1 -0
  47. package/dist/providers/manager.js +204 -0
  48. package/dist/providers/manager.js.map +1 -0
  49. package/dist/providers/provider.d.ts +29 -0
  50. package/dist/providers/provider.d.ts.map +1 -0
  51. package/dist/providers/provider.js +179 -0
  52. package/dist/providers/provider.js.map +1 -0
  53. package/dist/providers/types.d.ts +69 -0
  54. package/dist/providers/types.d.ts.map +1 -0
  55. package/dist/providers/types.js +2 -0
  56. package/dist/providers/types.js.map +1 -0
  57. package/dist/server.d.ts +24 -0
  58. package/dist/server.d.ts.map +1 -0
  59. package/dist/server.js +501 -0
  60. package/dist/server.js.map +1 -0
  61. package/dist/services/approval.d.ts +44 -0
  62. package/dist/services/approval.d.ts.map +1 -0
  63. package/dist/services/approval.js +159 -0
  64. package/dist/services/approval.js.map +1 -0
  65. package/dist/services/cache.d.ts +21 -0
  66. package/dist/services/cache.d.ts.map +1 -0
  67. package/dist/services/cache.js +63 -0
  68. package/dist/services/cache.js.map +1 -0
  69. package/dist/services/conversation.d.ts +24 -0
  70. package/dist/services/conversation.d.ts.map +1 -0
  71. package/dist/services/conversation.js +108 -0
  72. package/dist/services/conversation.js.map +1 -0
  73. package/dist/services/function-bridge.d.ts +41 -0
  74. package/dist/services/function-bridge.d.ts.map +1 -0
  75. package/dist/services/function-bridge.js +259 -0
  76. package/dist/services/function-bridge.js.map +1 -0
  77. package/dist/services/health.d.ts +17 -0
  78. package/dist/services/health.d.ts.map +1 -0
  79. package/dist/services/health.js +77 -0
  80. package/dist/services/health.js.map +1 -0
  81. package/dist/services/mcp-client-manager.d.ts +49 -0
  82. package/dist/services/mcp-client-manager.d.ts.map +1 -0
  83. package/dist/services/mcp-client-manager.js +279 -0
  84. package/dist/services/mcp-client-manager.js.map +1 -0
  85. package/dist/tools/approve-mcp-request.d.ts +9 -0
  86. package/dist/tools/approve-mcp-request.d.ts.map +1 -0
  87. package/dist/tools/approve-mcp-request.js +111 -0
  88. package/dist/tools/approve-mcp-request.js.map +1 -0
  89. package/dist/tools/ask-duck.d.ts +9 -0
  90. package/dist/tools/ask-duck.d.ts.map +1 -0
  91. package/dist/tools/ask-duck.js +43 -0
  92. package/dist/tools/ask-duck.js.map +1 -0
  93. package/dist/tools/chat-duck.d.ts +9 -0
  94. package/dist/tools/chat-duck.d.ts.map +1 -0
  95. package/dist/tools/chat-duck.js +57 -0
  96. package/dist/tools/chat-duck.js.map +1 -0
  97. package/dist/tools/clear-conversations.d.ts +8 -0
  98. package/dist/tools/clear-conversations.d.ts.map +1 -0
  99. package/dist/tools/clear-conversations.js +17 -0
  100. package/dist/tools/clear-conversations.js.map +1 -0
  101. package/dist/tools/compare-ducks.d.ts +8 -0
  102. package/dist/tools/compare-ducks.d.ts.map +1 -0
  103. package/dist/tools/compare-ducks.js +49 -0
  104. package/dist/tools/compare-ducks.js.map +1 -0
  105. package/dist/tools/duck-council.d.ts +8 -0
  106. package/dist/tools/duck-council.d.ts.map +1 -0
  107. package/dist/tools/duck-council.js +69 -0
  108. package/dist/tools/duck-council.js.map +1 -0
  109. package/dist/tools/get-pending-approvals.d.ts +15 -0
  110. package/dist/tools/get-pending-approvals.d.ts.map +1 -0
  111. package/dist/tools/get-pending-approvals.js +74 -0
  112. package/dist/tools/get-pending-approvals.js.map +1 -0
  113. package/dist/tools/list-ducks.d.ts +9 -0
  114. package/dist/tools/list-ducks.d.ts.map +1 -0
  115. package/dist/tools/list-ducks.js +47 -0
  116. package/dist/tools/list-ducks.js.map +1 -0
  117. package/dist/tools/list-models.d.ts +8 -0
  118. package/dist/tools/list-models.d.ts.map +1 -0
  119. package/dist/tools/list-models.js +72 -0
  120. package/dist/tools/list-models.js.map +1 -0
  121. package/dist/tools/mcp-status.d.ts +17 -0
  122. package/dist/tools/mcp-status.d.ts.map +1 -0
  123. package/dist/tools/mcp-status.js +100 -0
  124. package/dist/tools/mcp-status.js.map +1 -0
  125. package/dist/utils/ascii-art.d.ts +19 -0
  126. package/dist/utils/ascii-art.d.ts.map +1 -0
  127. package/dist/utils/ascii-art.js +73 -0
  128. package/dist/utils/ascii-art.js.map +1 -0
  129. package/dist/utils/logger.d.ts +3 -0
  130. package/dist/utils/logger.d.ts.map +1 -0
  131. package/dist/utils/logger.js +86 -0
  132. package/dist/utils/logger.js.map +1 -0
  133. package/dist/utils/safe-logger.d.ts +23 -0
  134. package/dist/utils/safe-logger.d.ts.map +1 -0
  135. package/dist/utils/safe-logger.js +145 -0
  136. package/dist/utils/safe-logger.js.map +1 -0
  137. package/docker-compose.yml +161 -0
  138. package/jest.config.js +26 -0
  139. package/package.json +65 -0
  140. package/scripts/build-multiarch.sh +290 -0
  141. package/scripts/deploy-raspbian.sh +410 -0
  142. package/scripts/deploy.sh +322 -0
  143. package/scripts/gh-deploy.sh +343 -0
  144. package/scripts/setup-docker-raspbian.sh +530 -0
  145. package/server.json +8 -0
  146. package/src/config/config.ts +357 -0
  147. package/src/config/types.ts +89 -0
  148. package/src/index.ts +114 -0
  149. package/src/providers/duck-provider-enhanced.ts +294 -0
  150. package/src/providers/enhanced-manager.ts +290 -0
  151. package/src/providers/manager.ts +257 -0
  152. package/src/providers/provider.ts +207 -0
  153. package/src/providers/types.ts +78 -0
  154. package/src/server.ts +603 -0
  155. package/src/services/approval.ts +225 -0
  156. package/src/services/cache.ts +79 -0
  157. package/src/services/conversation.ts +146 -0
  158. package/src/services/function-bridge.ts +329 -0
  159. package/src/services/health.ts +107 -0
  160. package/src/services/mcp-client-manager.ts +362 -0
  161. package/src/tools/approve-mcp-request.ts +126 -0
  162. package/src/tools/ask-duck.ts +74 -0
  163. package/src/tools/chat-duck.ts +82 -0
  164. package/src/tools/clear-conversations.ts +24 -0
  165. package/src/tools/compare-ducks.ts +67 -0
  166. package/src/tools/duck-council.ts +88 -0
  167. package/src/tools/get-pending-approvals.ts +90 -0
  168. package/src/tools/list-ducks.ts +65 -0
  169. package/src/tools/list-models.ts +101 -0
  170. package/src/tools/mcp-status.ts +117 -0
  171. package/src/utils/ascii-art.ts +85 -0
  172. package/src/utils/logger.ts +116 -0
  173. package/src/utils/safe-logger.ts +165 -0
  174. package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
  175. package/systemd/mcp-rubber-duck.service +58 -0
  176. package/test-functionality.js +147 -0
  177. package/test-mcp-interface.js +221 -0
  178. package/tests/ascii-art.test.ts +36 -0
  179. package/tests/config.test.ts +239 -0
  180. package/tests/conversation.test.ts +308 -0
  181. package/tests/mcp-bridge.test.ts +291 -0
  182. package/tests/providers.test.ts +269 -0
  183. package/tests/tools/clear-conversations.test.ts +163 -0
  184. package/tsconfig.json +26 -0
@@ -0,0 +1,225 @@
1
+ import { logger } from '../utils/logger.js';
2
+ import { SafeLogger } from '../utils/safe-logger.js';
3
+ import { randomUUID } from 'crypto';
4
+
5
+ export interface ApprovalRequest {
6
+ id: string;
7
+ timestamp: number;
8
+ duckName: string;
9
+ mcpServer: string;
10
+ toolName: string;
11
+ arguments: Record<string, unknown>;
12
+ status: 'pending' | 'approved' | 'denied' | 'expired';
13
+ approvedBy?: string;
14
+ deniedReason?: string;
15
+ expiresAt: number;
16
+ }
17
+
18
+ export type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'expired';
19
+
20
+ export class ApprovalService {
21
+ private pendingApprovals: Map<string, ApprovalRequest> = new Map();
22
+ private approvalTimeout: number;
23
+ private cleanupInterval: NodeJS.Timeout | null = null;
24
+ private approvedToolsForSession: Set<string> = new Set();
25
+
26
+ constructor(approvalTimeoutSeconds: number = 300) {
27
+ this.approvalTimeout = approvalTimeoutSeconds * 1000; // Convert to milliseconds
28
+ this.startCleanupTimer();
29
+ }
30
+
31
+ createApprovalRequest(
32
+ duckName: string,
33
+ mcpServer: string,
34
+ toolName: string,
35
+ args: Record<string, unknown>
36
+ ): ApprovalRequest {
37
+ const id = randomUUID();
38
+ const now = Date.now();
39
+
40
+ const request: ApprovalRequest = {
41
+ id,
42
+ timestamp: now,
43
+ duckName,
44
+ mcpServer,
45
+ toolName,
46
+ arguments: args,
47
+ status: 'pending',
48
+ expiresAt: now + this.approvalTimeout,
49
+ };
50
+
51
+ this.pendingApprovals.set(id, request);
52
+
53
+ const safeMessage = SafeLogger.createApprovalMessage(duckName, mcpServer, toolName, args);
54
+ logger.info(`Created approval request ${id} for ${duckName} to call ${mcpServer}:${toolName}`);
55
+ SafeLogger.debug(`Approval request details:`, { id, duckName, mcpServer, toolName, safeMessage });
56
+
57
+ return request;
58
+ }
59
+
60
+ getApprovalRequest(id: string): ApprovalRequest | undefined {
61
+ const request = this.pendingApprovals.get(id);
62
+
63
+ // Check if expired
64
+ if (request && Date.now() > request.expiresAt && request.status === 'pending') {
65
+ request.status = 'expired';
66
+ logger.info(`Approval request ${id} has expired`);
67
+ }
68
+
69
+ return request;
70
+ }
71
+
72
+ getApprovalStatus(id: string): ApprovalStatus | undefined {
73
+ const request = this.getApprovalRequest(id);
74
+ return request?.status;
75
+ }
76
+
77
+ approveRequest(id: string, approvedBy: string = 'user'): boolean {
78
+ const request = this.getApprovalRequest(id);
79
+
80
+ if (!request) {
81
+ logger.warn(`Approval request ${id} not found`);
82
+ return false;
83
+ }
84
+
85
+ if (request.status !== 'pending') {
86
+ logger.warn(`Approval request ${id} is not pending (status: ${request.status})`);
87
+ return false;
88
+ }
89
+
90
+ if (Date.now() > request.expiresAt) {
91
+ request.status = 'expired';
92
+ logger.warn(`Approval request ${id} has expired`);
93
+ return false;
94
+ }
95
+
96
+ request.status = 'approved';
97
+ request.approvedBy = approvedBy;
98
+
99
+ // Mark tool as approved for this session
100
+ const sessionKey = this.createSessionKey(request.duckName, request.mcpServer, request.toolName);
101
+ this.approvedToolsForSession.add(sessionKey);
102
+
103
+ logger.info(`Approval request ${id} approved by ${approvedBy} - tool ${sessionKey} now approved for session`);
104
+ return true;
105
+ }
106
+
107
+ denyRequest(id: string, reason?: string): boolean {
108
+ const request = this.getApprovalRequest(id);
109
+
110
+ if (!request) {
111
+ logger.warn(`Approval request ${id} not found`);
112
+ return false;
113
+ }
114
+
115
+ if (request.status !== 'pending') {
116
+ logger.warn(`Approval request ${id} is not pending (status: ${request.status})`);
117
+ return false;
118
+ }
119
+
120
+ request.status = 'denied';
121
+ request.deniedReason = reason;
122
+
123
+ logger.info(`Approval request ${id} denied${reason ? `: ${reason}` : ''}`);
124
+ return true;
125
+ }
126
+
127
+ getPendingApprovals(): ApprovalRequest[] {
128
+ // Clean up expired requests first
129
+ this.cleanupExpired();
130
+
131
+ return Array.from(this.pendingApprovals.values())
132
+ .filter(request => request.status === 'pending');
133
+ }
134
+
135
+ getAllApprovals(): ApprovalRequest[] {
136
+ // Clean up expired requests first
137
+ this.cleanupExpired();
138
+
139
+ return Array.from(this.pendingApprovals.values());
140
+ }
141
+
142
+ getApprovalsByDuck(duckName: string): ApprovalRequest[] {
143
+ return Array.from(this.pendingApprovals.values())
144
+ .filter(request => request.duckName === duckName);
145
+ }
146
+
147
+ cleanupExpired(): number {
148
+ const now = Date.now();
149
+ let cleanedUp = 0;
150
+
151
+ for (const [id, request] of this.pendingApprovals.entries()) {
152
+ if (now > request.expiresAt && request.status === 'pending') {
153
+ request.status = 'expired';
154
+ logger.debug(`Marked approval request ${id} as expired`);
155
+ cleanedUp++;
156
+ }
157
+ }
158
+
159
+ return cleanedUp;
160
+ }
161
+
162
+ private startCleanupTimer(): void {
163
+ // Clean up expired requests every minute
164
+ this.cleanupInterval = setInterval(() => {
165
+ const cleaned = this.cleanupExpired();
166
+ if (cleaned > 0) {
167
+ logger.debug(`Cleaned up ${cleaned} expired approval requests`);
168
+ }
169
+ }, 60000);
170
+ }
171
+
172
+ shutdown(): void {
173
+ if (this.cleanupInterval) {
174
+ clearInterval(this.cleanupInterval);
175
+ this.cleanupInterval = null;
176
+ }
177
+ }
178
+
179
+ // Session-based approval methods
180
+ private createSessionKey(duckName: string, mcpServer: string, toolName: string): string {
181
+ return `${duckName}:${mcpServer}:${toolName}`;
182
+ }
183
+
184
+ isToolApprovedForSession(duckName: string, mcpServer: string, toolName: string): boolean {
185
+ const sessionKey = this.createSessionKey(duckName, mcpServer, toolName);
186
+ return this.approvedToolsForSession.has(sessionKey);
187
+ }
188
+
189
+ markToolAsApprovedForSession(duckName: string, mcpServer: string, toolName: string): void {
190
+ const sessionKey = this.createSessionKey(duckName, mcpServer, toolName);
191
+ this.approvedToolsForSession.add(sessionKey);
192
+ logger.info(`Tool ${sessionKey} marked as approved for session`);
193
+ }
194
+
195
+ clearSessionApprovals(): void {
196
+ const count = this.approvedToolsForSession.size;
197
+ this.approvedToolsForSession.clear();
198
+ logger.info(`Cleared ${count} session approvals`);
199
+ }
200
+
201
+ getSessionApprovals(): string[] {
202
+ return Array.from(this.approvedToolsForSession);
203
+ }
204
+
205
+ // For debugging/admin purposes
206
+ getStats(): {
207
+ total: number;
208
+ pending: number;
209
+ approved: number;
210
+ denied: number;
211
+ expired: number;
212
+ } {
213
+ this.cleanupExpired();
214
+
215
+ const all = Array.from(this.pendingApprovals.values());
216
+
217
+ return {
218
+ total: all.length,
219
+ pending: all.filter(r => r.status === 'pending').length,
220
+ approved: all.filter(r => r.status === 'approved').length,
221
+ denied: all.filter(r => r.status === 'denied').length,
222
+ expired: all.filter(r => r.status === 'expired').length,
223
+ };
224
+ }
225
+ }
@@ -0,0 +1,79 @@
1
+ import NodeCache from 'node-cache';
2
+ import { createHash } from 'crypto';
3
+ import { logger } from '../utils/logger.js';
4
+
5
+ export class ResponseCache {
6
+ private cache: NodeCache;
7
+
8
+ constructor(ttlSeconds: number = 300) {
9
+ this.cache = new NodeCache({
10
+ stdTTL: ttlSeconds,
11
+ checkperiod: ttlSeconds * 0.2,
12
+ useClones: false,
13
+ });
14
+
15
+ this.cache.on('expired', (key) => {
16
+ logger.debug(`Cache expired for key: ${key}`);
17
+ });
18
+ }
19
+
20
+ generateKey(provider: string, prompt: string, options?: Record<string, unknown>): string {
21
+ const data = JSON.stringify({ provider, prompt, options });
22
+ return createHash('sha256').update(data).digest('hex');
23
+ }
24
+
25
+ get<T>(key: string): T | undefined {
26
+ return this.cache.get<T>(key);
27
+ }
28
+
29
+ set<T>(key: string, value: T, ttl?: number): boolean {
30
+ if (ttl !== undefined) {
31
+ return this.cache.set(key, value, ttl);
32
+ } else {
33
+ return this.cache.set(key, value);
34
+ }
35
+ }
36
+
37
+ has(key: string): boolean {
38
+ return this.cache.has(key);
39
+ }
40
+
41
+ delete(key: string): number {
42
+ return this.cache.del(key);
43
+ }
44
+
45
+ flush(): void {
46
+ this.cache.flushAll();
47
+ logger.debug('Cache flushed');
48
+ }
49
+
50
+ getStats() {
51
+ const stats = this.cache.getStats();
52
+ return {
53
+ keys: this.cache.keys().length,
54
+ hits: stats.hits,
55
+ misses: stats.misses,
56
+ hitRate: stats.hits / (stats.hits + stats.misses) || 0,
57
+ };
58
+ }
59
+
60
+ // Helper method for caching provider responses
61
+ async getOrSet<T>(
62
+ key: string,
63
+ fetcher: () => Promise<T>,
64
+ ttl?: number
65
+ ): Promise<{ value: T; cached: boolean }> {
66
+ const cached = this.get<T>(key);
67
+
68
+ if (cached !== undefined) {
69
+ logger.debug(`Cache hit for key: ${key}`);
70
+ return { value: cached, cached: true };
71
+ }
72
+
73
+ logger.debug(`Cache miss for key: ${key}`);
74
+ const value = await fetcher();
75
+ this.set(key, value, ttl);
76
+
77
+ return { value, cached: false };
78
+ }
79
+ }
@@ -0,0 +1,146 @@
1
+ import { Conversation, ConversationMessage } from '../config/types.js';
2
+ import { logger } from '../utils/logger.js';
3
+
4
+ export class ConversationManager {
5
+ private conversations: Map<string, Conversation> = new Map();
6
+ private maxConversationSize = 50; // Maximum messages per conversation
7
+
8
+ createConversation(id: string, provider: string): Conversation {
9
+ const conversation: Conversation = {
10
+ id,
11
+ messages: [],
12
+ provider,
13
+ createdAt: new Date(),
14
+ updatedAt: new Date(),
15
+ };
16
+
17
+ this.conversations.set(id, conversation);
18
+ logger.debug(`Created new conversation: ${id}`);
19
+ return conversation;
20
+ }
21
+
22
+ getConversation(id: string): Conversation | undefined {
23
+ return this.conversations.get(id);
24
+ }
25
+
26
+ addMessage(
27
+ conversationId: string,
28
+ message: ConversationMessage
29
+ ): Conversation {
30
+ const conversation = this.conversations.get(conversationId);
31
+
32
+ if (!conversation) {
33
+ throw new Error(`Conversation ${conversationId} not found`);
34
+ }
35
+
36
+ conversation.messages.push(message);
37
+ conversation.updatedAt = new Date();
38
+
39
+ // Trim conversation if too long
40
+ if (conversation.messages.length > this.maxConversationSize) {
41
+ const toRemove = conversation.messages.length - this.maxConversationSize;
42
+ conversation.messages = conversation.messages.slice(toRemove);
43
+ logger.debug(`Trimmed ${toRemove} messages from conversation ${conversationId}`);
44
+ }
45
+
46
+ this.conversations.set(conversationId, conversation);
47
+ return conversation;
48
+ }
49
+
50
+ switchProvider(conversationId: string, newProvider: string): Conversation {
51
+ const conversation = this.conversations.get(conversationId);
52
+
53
+ if (!conversation) {
54
+ throw new Error(`Conversation ${conversationId} not found`);
55
+ }
56
+
57
+ conversation.provider = newProvider;
58
+ conversation.updatedAt = new Date();
59
+
60
+ // Add a system message noting the provider switch
61
+ conversation.messages.push({
62
+ role: 'system',
63
+ content: `Switched to ${newProvider} duck`,
64
+ timestamp: new Date(),
65
+ provider: newProvider,
66
+ });
67
+
68
+ this.conversations.set(conversationId, conversation);
69
+ return conversation;
70
+ }
71
+
72
+ listConversations(): Array<{
73
+ id: string;
74
+ provider: string;
75
+ messageCount: number;
76
+ createdAt: Date;
77
+ updatedAt: Date;
78
+ }> {
79
+ return Array.from(this.conversations.values()).map(conv => ({
80
+ id: conv.id,
81
+ provider: conv.provider,
82
+ messageCount: conv.messages.length,
83
+ createdAt: conv.createdAt,
84
+ updatedAt: conv.updatedAt,
85
+ }));
86
+ }
87
+
88
+ deleteConversation(id: string): boolean {
89
+ const deleted = this.conversations.delete(id);
90
+ if (deleted) {
91
+ logger.debug(`Deleted conversation: ${id}`);
92
+ }
93
+ return deleted;
94
+ }
95
+
96
+ clearOldConversations(maxAge: number = 24 * 60 * 60 * 1000) {
97
+ const now = Date.now();
98
+ let deleted = 0;
99
+
100
+ for (const [id, conversation] of this.conversations) {
101
+ if (now - conversation.updatedAt.getTime() > maxAge) {
102
+ this.conversations.delete(id);
103
+ deleted++;
104
+ }
105
+ }
106
+
107
+ if (deleted > 0) {
108
+ logger.info(`Cleared ${deleted} old conversations`);
109
+ }
110
+ }
111
+
112
+ getConversationContext(id: string, maxMessages?: number): ConversationMessage[] {
113
+ const conversation = this.conversations.get(id);
114
+
115
+ if (!conversation) {
116
+ return [];
117
+ }
118
+
119
+ const messages = conversation.messages;
120
+
121
+ if (maxMessages && messages.length > maxMessages) {
122
+ return messages.slice(-maxMessages);
123
+ }
124
+
125
+ return messages;
126
+ }
127
+
128
+ clearAll(): { conversationsCleared: number; messagesCleared: number } {
129
+ let totalMessages = 0;
130
+
131
+ // Count total messages across all conversations
132
+ for (const conversation of this.conversations.values()) {
133
+ totalMessages += conversation.messages.length;
134
+ }
135
+
136
+ const conversationsCleared = this.conversations.size;
137
+ this.conversations.clear();
138
+
139
+ logger.info(`Cleared ${conversationsCleared} conversations with ${totalMessages} total messages`);
140
+
141
+ return {
142
+ conversationsCleared,
143
+ messagesCleared: totalMessages,
144
+ };
145
+ }
146
+ }