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.
- package/.dockerignore +19 -0
- package/.env.desktop.example +145 -0
- package/.env.example +45 -0
- package/.env.pi.example +106 -0
- package/.env.template +165 -0
- package/.eslintrc.json +40 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
- package/.github/ISSUE_TEMPLATE/question.md +67 -0
- package/.github/pull_request_template.md +111 -0
- package/.github/workflows/docker-build.yml +138 -0
- package/.github/workflows/release.yml +182 -0
- package/.github/workflows/security.yml +141 -0
- package/.github/workflows/semantic-release.yml +89 -0
- package/.prettierrc +10 -0
- package/.releaserc.json +66 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +242 -0
- package/Dockerfile +62 -0
- package/LICENSE +21 -0
- package/README.md +803 -0
- package/audit-ci.json +8 -0
- package/config/claude_desktop.json +14 -0
- package/config/config.example.json +91 -0
- package/dist/config/config.d.ts +51 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +301 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/types.d.ts +356 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +41 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/duck-provider-enhanced.d.ts +29 -0
- package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
- package/dist/providers/duck-provider-enhanced.js +230 -0
- package/dist/providers/duck-provider-enhanced.js.map +1 -0
- package/dist/providers/enhanced-manager.d.ts +54 -0
- package/dist/providers/enhanced-manager.d.ts.map +1 -0
- package/dist/providers/enhanced-manager.js +217 -0
- package/dist/providers/enhanced-manager.js.map +1 -0
- package/dist/providers/manager.d.ts +28 -0
- package/dist/providers/manager.d.ts.map +1 -0
- package/dist/providers/manager.js +204 -0
- package/dist/providers/manager.js.map +1 -0
- package/dist/providers/provider.d.ts +29 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +179 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/providers/types.d.ts +69 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +501 -0
- package/dist/server.js.map +1 -0
- package/dist/services/approval.d.ts +44 -0
- package/dist/services/approval.d.ts.map +1 -0
- package/dist/services/approval.js +159 -0
- package/dist/services/approval.js.map +1 -0
- package/dist/services/cache.d.ts +21 -0
- package/dist/services/cache.d.ts.map +1 -0
- package/dist/services/cache.js +63 -0
- package/dist/services/cache.js.map +1 -0
- package/dist/services/conversation.d.ts +24 -0
- package/dist/services/conversation.d.ts.map +1 -0
- package/dist/services/conversation.js +108 -0
- package/dist/services/conversation.js.map +1 -0
- package/dist/services/function-bridge.d.ts +41 -0
- package/dist/services/function-bridge.d.ts.map +1 -0
- package/dist/services/function-bridge.js +259 -0
- package/dist/services/function-bridge.js.map +1 -0
- package/dist/services/health.d.ts +17 -0
- package/dist/services/health.d.ts.map +1 -0
- package/dist/services/health.js +77 -0
- package/dist/services/health.js.map +1 -0
- package/dist/services/mcp-client-manager.d.ts +49 -0
- package/dist/services/mcp-client-manager.d.ts.map +1 -0
- package/dist/services/mcp-client-manager.js +279 -0
- package/dist/services/mcp-client-manager.js.map +1 -0
- package/dist/tools/approve-mcp-request.d.ts +9 -0
- package/dist/tools/approve-mcp-request.d.ts.map +1 -0
- package/dist/tools/approve-mcp-request.js +111 -0
- package/dist/tools/approve-mcp-request.js.map +1 -0
- package/dist/tools/ask-duck.d.ts +9 -0
- package/dist/tools/ask-duck.d.ts.map +1 -0
- package/dist/tools/ask-duck.js +43 -0
- package/dist/tools/ask-duck.js.map +1 -0
- package/dist/tools/chat-duck.d.ts +9 -0
- package/dist/tools/chat-duck.d.ts.map +1 -0
- package/dist/tools/chat-duck.js +57 -0
- package/dist/tools/chat-duck.js.map +1 -0
- package/dist/tools/clear-conversations.d.ts +8 -0
- package/dist/tools/clear-conversations.d.ts.map +1 -0
- package/dist/tools/clear-conversations.js +17 -0
- package/dist/tools/clear-conversations.js.map +1 -0
- package/dist/tools/compare-ducks.d.ts +8 -0
- package/dist/tools/compare-ducks.d.ts.map +1 -0
- package/dist/tools/compare-ducks.js +49 -0
- package/dist/tools/compare-ducks.js.map +1 -0
- package/dist/tools/duck-council.d.ts +8 -0
- package/dist/tools/duck-council.d.ts.map +1 -0
- package/dist/tools/duck-council.js +69 -0
- package/dist/tools/duck-council.js.map +1 -0
- package/dist/tools/get-pending-approvals.d.ts +15 -0
- package/dist/tools/get-pending-approvals.d.ts.map +1 -0
- package/dist/tools/get-pending-approvals.js +74 -0
- package/dist/tools/get-pending-approvals.js.map +1 -0
- package/dist/tools/list-ducks.d.ts +9 -0
- package/dist/tools/list-ducks.d.ts.map +1 -0
- package/dist/tools/list-ducks.js +47 -0
- package/dist/tools/list-ducks.js.map +1 -0
- package/dist/tools/list-models.d.ts +8 -0
- package/dist/tools/list-models.d.ts.map +1 -0
- package/dist/tools/list-models.js +72 -0
- package/dist/tools/list-models.js.map +1 -0
- package/dist/tools/mcp-status.d.ts +17 -0
- package/dist/tools/mcp-status.d.ts.map +1 -0
- package/dist/tools/mcp-status.js +100 -0
- package/dist/tools/mcp-status.js.map +1 -0
- package/dist/utils/ascii-art.d.ts +19 -0
- package/dist/utils/ascii-art.d.ts.map +1 -0
- package/dist/utils/ascii-art.js +73 -0
- package/dist/utils/ascii-art.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +86 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/safe-logger.d.ts +23 -0
- package/dist/utils/safe-logger.d.ts.map +1 -0
- package/dist/utils/safe-logger.js +145 -0
- package/dist/utils/safe-logger.js.map +1 -0
- package/docker-compose.yml +161 -0
- package/jest.config.js +26 -0
- package/package.json +65 -0
- package/scripts/build-multiarch.sh +290 -0
- package/scripts/deploy-raspbian.sh +410 -0
- package/scripts/deploy.sh +322 -0
- package/scripts/gh-deploy.sh +343 -0
- package/scripts/setup-docker-raspbian.sh +530 -0
- package/server.json +8 -0
- package/src/config/config.ts +357 -0
- package/src/config/types.ts +89 -0
- package/src/index.ts +114 -0
- package/src/providers/duck-provider-enhanced.ts +294 -0
- package/src/providers/enhanced-manager.ts +290 -0
- package/src/providers/manager.ts +257 -0
- package/src/providers/provider.ts +207 -0
- package/src/providers/types.ts +78 -0
- package/src/server.ts +603 -0
- package/src/services/approval.ts +225 -0
- package/src/services/cache.ts +79 -0
- package/src/services/conversation.ts +146 -0
- package/src/services/function-bridge.ts +329 -0
- package/src/services/health.ts +107 -0
- package/src/services/mcp-client-manager.ts +362 -0
- package/src/tools/approve-mcp-request.ts +126 -0
- package/src/tools/ask-duck.ts +74 -0
- package/src/tools/chat-duck.ts +82 -0
- package/src/tools/clear-conversations.ts +24 -0
- package/src/tools/compare-ducks.ts +67 -0
- package/src/tools/duck-council.ts +88 -0
- package/src/tools/get-pending-approvals.ts +90 -0
- package/src/tools/list-ducks.ts +65 -0
- package/src/tools/list-models.ts +101 -0
- package/src/tools/mcp-status.ts +117 -0
- package/src/utils/ascii-art.ts +85 -0
- package/src/utils/logger.ts +116 -0
- package/src/utils/safe-logger.ts +165 -0
- package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
- package/systemd/mcp-rubber-duck.service +58 -0
- package/test-functionality.js +147 -0
- package/test-mcp-interface.js +221 -0
- package/tests/ascii-art.test.ts +36 -0
- package/tests/config.test.ts +239 -0
- package/tests/conversation.test.ts +308 -0
- package/tests/mcp-bridge.test.ts +291 -0
- package/tests/providers.test.ts +269 -0
- package/tests/tools/clear-conversations.test.ts +163 -0
- 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
|
+
}
|