claude-recall 0.2.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/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/cli/claude-recall-cli.js +345 -0
- package/dist/cli/commands/migrate.js +245 -0
- package/dist/core/pattern-detector.js +242 -0
- package/dist/core/patterns.js +56 -0
- package/dist/core/retrieval.js +108 -0
- package/dist/mcp/rate-limiter.js +114 -0
- package/dist/mcp/server.js +283 -0
- package/dist/mcp/session-manager.js +161 -0
- package/dist/mcp/tools/memory-tools.js +358 -0
- package/dist/mcp/transports/stdio.js +246 -0
- package/dist/memory/pattern-store.js +66 -0
- package/dist/memory/schema.sql +24 -0
- package/dist/memory/storage.js +274 -0
- package/dist/services/action-pattern-detector.js +251 -0
- package/dist/services/claude-json-watcher.js +243 -0
- package/dist/services/config.js +149 -0
- package/dist/services/database-manager.js +332 -0
- package/dist/services/logging.js +124 -0
- package/dist/services/memory-enhancer.js +148 -0
- package/dist/services/memory.js +334 -0
- package/dist/services/pattern-service.js +172 -0
- package/dist/services/preference-extractor.js +286 -0
- package/dist/services/semantic-preference-extractor.js +432 -0
- package/docs/MCP_API_REFERENCE.md +257 -0
- package/docs/MCP_USER_GUIDE.md +115 -0
- package/docs/release-process.md +86 -0
- package/package.json +89 -0
- package/scripts/postinstall.js +76 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RateLimiter = void 0;
|
|
4
|
+
class RateLimiter {
|
|
5
|
+
constructor(logger, config = {}) {
|
|
6
|
+
this.logger = logger;
|
|
7
|
+
this.requests = new Map();
|
|
8
|
+
this.cleanupInterval = null;
|
|
9
|
+
this.windowMs = config.windowMs || 60000; // 1 minute default
|
|
10
|
+
this.maxRequests = config.maxRequests || 100; // 100 requests default
|
|
11
|
+
this.skipSuccessfulRequests = config.skipSuccessfulRequests || false;
|
|
12
|
+
// Clean up old entries periodically
|
|
13
|
+
this.cleanupInterval = setInterval(() => {
|
|
14
|
+
this.cleanup();
|
|
15
|
+
}, this.windowMs);
|
|
16
|
+
this.logger.info('RateLimiter', 'Rate limiter initialized', {
|
|
17
|
+
windowMs: this.windowMs,
|
|
18
|
+
maxRequests: this.maxRequests,
|
|
19
|
+
skipSuccessfulRequests: this.skipSuccessfulRequests
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async checkLimit(sessionId) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const requests = this.requests.get(sessionId) || [];
|
|
25
|
+
// Remove old requests outside window
|
|
26
|
+
const validRequests = requests.filter(time => now - time < this.windowMs);
|
|
27
|
+
if (validRequests.length >= this.maxRequests) {
|
|
28
|
+
this.logger.warn('RateLimiter', 'Rate limit exceeded', {
|
|
29
|
+
sessionId,
|
|
30
|
+
requests: validRequests.length,
|
|
31
|
+
window: this.windowMs,
|
|
32
|
+
maxRequests: this.maxRequests
|
|
33
|
+
});
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// Don't add to count yet - wait for recordRequest
|
|
37
|
+
this.requests.set(sessionId, validRequests);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
recordRequest(sessionId, successful = true) {
|
|
41
|
+
if (this.skipSuccessfulRequests && successful) {
|
|
42
|
+
return; // Don't count successful requests if configured
|
|
43
|
+
}
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const requests = this.requests.get(sessionId) || [];
|
|
46
|
+
// Add new request
|
|
47
|
+
requests.push(now);
|
|
48
|
+
// Keep only requests within window
|
|
49
|
+
const validRequests = requests.filter(time => now - time < this.windowMs);
|
|
50
|
+
this.requests.set(sessionId, validRequests);
|
|
51
|
+
this.logger.debug('RateLimiter', 'Request recorded', {
|
|
52
|
+
sessionId,
|
|
53
|
+
requestCount: validRequests.length,
|
|
54
|
+
successful
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
getRemainingRequests(sessionId) {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const requests = this.requests.get(sessionId) || [];
|
|
60
|
+
const validRequests = requests.filter(time => now - time < this.windowMs);
|
|
61
|
+
return Math.max(0, this.maxRequests - validRequests.length);
|
|
62
|
+
}
|
|
63
|
+
resetLimit(sessionId) {
|
|
64
|
+
this.requests.delete(sessionId);
|
|
65
|
+
this.logger.info('RateLimiter', 'Rate limit reset', { sessionId });
|
|
66
|
+
}
|
|
67
|
+
cleanup() {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
let cleaned = 0;
|
|
70
|
+
for (const [sessionId, requests] of this.requests) {
|
|
71
|
+
const validRequests = requests.filter(time => now - time < this.windowMs);
|
|
72
|
+
if (validRequests.length === 0) {
|
|
73
|
+
this.requests.delete(sessionId);
|
|
74
|
+
cleaned++;
|
|
75
|
+
}
|
|
76
|
+
else if (validRequests.length < requests.length) {
|
|
77
|
+
this.requests.set(sessionId, validRequests);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (cleaned > 0) {
|
|
81
|
+
this.logger.debug('RateLimiter', `Cleaned up ${cleaned} expired session limits`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
getStats() {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const stats = {
|
|
87
|
+
activeSessions: this.requests.size,
|
|
88
|
+
totalRequests: 0,
|
|
89
|
+
topSessions: []
|
|
90
|
+
};
|
|
91
|
+
const sessionRequests = [];
|
|
92
|
+
for (const [sessionId, requests] of this.requests) {
|
|
93
|
+
const validRequests = requests.filter(time => now - time < this.windowMs);
|
|
94
|
+
const count = validRequests.length;
|
|
95
|
+
stats.totalRequests += count;
|
|
96
|
+
sessionRequests.push({ sessionId, requests: count });
|
|
97
|
+
}
|
|
98
|
+
// Get top 5 sessions by request count
|
|
99
|
+
stats.topSessions = sessionRequests
|
|
100
|
+
.sort((a, b) => b.requests - a.requests)
|
|
101
|
+
.slice(0, 5);
|
|
102
|
+
return stats;
|
|
103
|
+
}
|
|
104
|
+
shutdown() {
|
|
105
|
+
if (this.cleanupInterval) {
|
|
106
|
+
clearInterval(this.cleanupInterval);
|
|
107
|
+
this.cleanupInterval = null;
|
|
108
|
+
}
|
|
109
|
+
this.logger.info('RateLimiter', 'Rate limiter shut down', {
|
|
110
|
+
activeSessions: this.requests.size
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.RateLimiter = RateLimiter;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MCPServer = void 0;
|
|
4
|
+
const stdio_1 = require("./transports/stdio");
|
|
5
|
+
const memory_tools_1 = require("./tools/memory-tools");
|
|
6
|
+
const memory_1 = require("../services/memory");
|
|
7
|
+
const logging_1 = require("../services/logging");
|
|
8
|
+
const session_manager_1 = require("./session-manager");
|
|
9
|
+
const rate_limiter_1 = require("./rate-limiter");
|
|
10
|
+
class MCPServer {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.tools = new Map();
|
|
13
|
+
this.sessions = new Map();
|
|
14
|
+
this.isInitialized = false;
|
|
15
|
+
this.transport = new stdio_1.StdioTransport();
|
|
16
|
+
this.memoryService = memory_1.MemoryService.getInstance();
|
|
17
|
+
this.logger = logging_1.LoggingService.getInstance();
|
|
18
|
+
this.sessionManager = new session_manager_1.SessionManager(this.logger);
|
|
19
|
+
this.rateLimiter = new rate_limiter_1.RateLimiter(this.logger, {
|
|
20
|
+
windowMs: 60000, // 1 minute
|
|
21
|
+
maxRequests: 100, // 100 requests per minute
|
|
22
|
+
skipSuccessfulRequests: false
|
|
23
|
+
});
|
|
24
|
+
this.setupRequestHandlers();
|
|
25
|
+
this.registerTools();
|
|
26
|
+
}
|
|
27
|
+
setupRequestHandlers() {
|
|
28
|
+
this.transport.onRequest(async (request) => {
|
|
29
|
+
try {
|
|
30
|
+
switch (request.method) {
|
|
31
|
+
case 'initialize':
|
|
32
|
+
return this.handleInitialize(request);
|
|
33
|
+
case 'tools/list':
|
|
34
|
+
return this.handleToolsList(request);
|
|
35
|
+
case 'tools/call':
|
|
36
|
+
return this.handleToolCall(request);
|
|
37
|
+
case 'notifications/initialized':
|
|
38
|
+
return this.handleInitialized(request);
|
|
39
|
+
case 'health/check':
|
|
40
|
+
return this.handleHealthCheck(request);
|
|
41
|
+
default:
|
|
42
|
+
return this.createErrorResponse(request.id, -32601, `Method not found: ${request.method}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.logger.logServiceError('MCPServer', 'handleRequest', error, { method: request.method });
|
|
47
|
+
return this.createErrorResponse(request.id, -32603, 'Internal error', { message: error.message });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
registerTools() {
|
|
52
|
+
const memoryTools = new memory_tools_1.MemoryTools(this.memoryService, this.logger);
|
|
53
|
+
const tools = memoryTools.getTools();
|
|
54
|
+
for (const tool of tools) {
|
|
55
|
+
this.tools.set(tool.name, tool);
|
|
56
|
+
}
|
|
57
|
+
this.logger.info('MCPServer', `Registered ${this.tools.size} tools`, {
|
|
58
|
+
tools: Array.from(this.tools.keys())
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async handleInitialize(request) {
|
|
62
|
+
const params = request.params || {};
|
|
63
|
+
this.logger.info('MCPServer', 'Initializing MCP server', params);
|
|
64
|
+
return {
|
|
65
|
+
jsonrpc: "2.0",
|
|
66
|
+
id: request.id,
|
|
67
|
+
result: {
|
|
68
|
+
protocolVersion: "2024-11-05",
|
|
69
|
+
capabilities: {
|
|
70
|
+
tools: {},
|
|
71
|
+
logging: {}
|
|
72
|
+
},
|
|
73
|
+
serverInfo: {
|
|
74
|
+
name: "claude-recall",
|
|
75
|
+
version: "0.2.0"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async handleInitialized(request) {
|
|
81
|
+
this.isInitialized = true;
|
|
82
|
+
this.logger.info('MCPServer', 'MCP server initialized successfully');
|
|
83
|
+
// Note: initialized is a notification, no response required
|
|
84
|
+
return {
|
|
85
|
+
jsonrpc: "2.0",
|
|
86
|
+
id: request.id,
|
|
87
|
+
result: null
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async handleToolsList(request) {
|
|
91
|
+
const toolList = Array.from(this.tools.values()).map(tool => ({
|
|
92
|
+
name: tool.name,
|
|
93
|
+
description: tool.description,
|
|
94
|
+
inputSchema: tool.inputSchema
|
|
95
|
+
}));
|
|
96
|
+
this.logger.debug('MCPServer', `Listing ${toolList.length} tools`);
|
|
97
|
+
return {
|
|
98
|
+
jsonrpc: "2.0",
|
|
99
|
+
id: request.id,
|
|
100
|
+
result: {
|
|
101
|
+
tools: toolList
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async handleToolCall(request) {
|
|
106
|
+
const { name, arguments: toolArgs } = request.params || {};
|
|
107
|
+
if (!name || typeof name !== 'string') {
|
|
108
|
+
return this.createErrorResponse(request.id, -32602, 'Invalid params: tool name required');
|
|
109
|
+
}
|
|
110
|
+
const tool = this.tools.get(name);
|
|
111
|
+
if (!tool) {
|
|
112
|
+
return this.createErrorResponse(request.id, -32601, `Tool not found: ${name}`);
|
|
113
|
+
}
|
|
114
|
+
const startTime = Date.now();
|
|
115
|
+
try {
|
|
116
|
+
// Create or get session context
|
|
117
|
+
const sessionId = toolArgs?.sessionId || this.generateSessionId();
|
|
118
|
+
// Get or create session
|
|
119
|
+
let session = this.sessionManager.getSession(sessionId);
|
|
120
|
+
if (!session) {
|
|
121
|
+
session = this.sessionManager.createSession(sessionId);
|
|
122
|
+
}
|
|
123
|
+
// Check rate limit
|
|
124
|
+
const withinLimit = await this.rateLimiter.checkLimit(sessionId);
|
|
125
|
+
if (!withinLimit) {
|
|
126
|
+
const remaining = this.rateLimiter.getRemainingRequests(sessionId);
|
|
127
|
+
return this.createErrorResponse(request.id, -32000, // Custom error code for rate limit
|
|
128
|
+
'Rate limit exceeded', {
|
|
129
|
+
sessionId,
|
|
130
|
+
remainingRequests: remaining,
|
|
131
|
+
windowMs: 60000,
|
|
132
|
+
message: 'Too many requests. Please wait before trying again.'
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Update session activity
|
|
136
|
+
this.sessionManager.incrementToolCalls(sessionId);
|
|
137
|
+
const context = {
|
|
138
|
+
sessionId,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
projectId: toolArgs?.projectId
|
|
141
|
+
};
|
|
142
|
+
this.logger.info('MCPServer', `Executing tool: ${name}`, {
|
|
143
|
+
sessionId,
|
|
144
|
+
args: toolArgs,
|
|
145
|
+
toolCallCount: session.toolCalls
|
|
146
|
+
});
|
|
147
|
+
const result = await tool.handler(toolArgs || {}, context);
|
|
148
|
+
// Record successful request for rate limiting
|
|
149
|
+
this.rateLimiter.recordRequest(sessionId, true);
|
|
150
|
+
// Claude-flow pattern: Enhanced response format
|
|
151
|
+
return {
|
|
152
|
+
jsonrpc: "2.0",
|
|
153
|
+
id: request.id,
|
|
154
|
+
result: {
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
isError: false,
|
|
162
|
+
metadata: {
|
|
163
|
+
toolName: name,
|
|
164
|
+
duration: Date.now() - startTime,
|
|
165
|
+
sessionId: context.sessionId
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
this.logger.logServiceError('MCPServer', `tool:${name}`, error, toolArgs);
|
|
172
|
+
// Record failed request for rate limiting
|
|
173
|
+
const sessionId = toolArgs?.sessionId || this.generateSessionId();
|
|
174
|
+
this.rateLimiter.recordRequest(sessionId, false);
|
|
175
|
+
// Claude-flow pattern: Enhanced error response
|
|
176
|
+
return {
|
|
177
|
+
jsonrpc: "2.0",
|
|
178
|
+
id: request.id,
|
|
179
|
+
result: {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: `Tool execution failed: ${error.message}`
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
isError: true,
|
|
187
|
+
metadata: {
|
|
188
|
+
toolName: name,
|
|
189
|
+
duration: Date.now() - startTime,
|
|
190
|
+
sessionId: this.generateSessionId(),
|
|
191
|
+
error: {
|
|
192
|
+
message: error.message,
|
|
193
|
+
stack: error.stack
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
createErrorResponse(id, code, message, data) {
|
|
201
|
+
return {
|
|
202
|
+
jsonrpc: "2.0",
|
|
203
|
+
id,
|
|
204
|
+
error: {
|
|
205
|
+
code,
|
|
206
|
+
message,
|
|
207
|
+
...(data && { data })
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
generateSessionId() {
|
|
212
|
+
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
213
|
+
}
|
|
214
|
+
async handleHealthCheck(request) {
|
|
215
|
+
const rateLimiterStats = this.rateLimiter.getStats();
|
|
216
|
+
const health = {
|
|
217
|
+
status: 'healthy',
|
|
218
|
+
version: '0.2.0',
|
|
219
|
+
uptime: process.uptime(),
|
|
220
|
+
memory: process.memoryUsage(),
|
|
221
|
+
sessions: {
|
|
222
|
+
total: this.sessionManager.getAllSessions().length,
|
|
223
|
+
active: this.sessionManager.getActiveSessionCount()
|
|
224
|
+
},
|
|
225
|
+
toolsRegistered: this.tools.size,
|
|
226
|
+
database: this.memoryService.isConnected() ? 'connected' : 'disconnected',
|
|
227
|
+
rateLimiter: {
|
|
228
|
+
activeSessions: rateLimiterStats.activeSessions,
|
|
229
|
+
totalRequests: rateLimiterStats.totalRequests,
|
|
230
|
+
topSessions: rateLimiterStats.topSessions
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
this.logger.info('MCPServer', 'Health check performed', health);
|
|
234
|
+
return {
|
|
235
|
+
jsonrpc: "2.0",
|
|
236
|
+
id: request.id,
|
|
237
|
+
result: health
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async start() {
|
|
241
|
+
try {
|
|
242
|
+
this.logger.info('MCPServer', 'Starting Claude Recall MCP server...');
|
|
243
|
+
await this.transport.start();
|
|
244
|
+
this.logger.info('MCPServer', 'MCP server started successfully');
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
this.logger.logServiceError('MCPServer', 'start', error);
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async stop() {
|
|
252
|
+
try {
|
|
253
|
+
this.logger.info('MCPServer', 'Stopping MCP server...');
|
|
254
|
+
// Clean up old sessions before shutdown
|
|
255
|
+
this.sessionManager.cleanupOldSessions();
|
|
256
|
+
// Shutdown session manager (persists sessions)
|
|
257
|
+
this.sessionManager.shutdown();
|
|
258
|
+
// Shutdown rate limiter
|
|
259
|
+
this.rateLimiter.shutdown();
|
|
260
|
+
await this.transport.stop();
|
|
261
|
+
this.memoryService.close();
|
|
262
|
+
this.logger.info('MCPServer', 'MCP server stopped');
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
this.logger.logServiceError('MCPServer', 'stop', error);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Graceful shutdown handling
|
|
270
|
+
setupSignalHandlers() {
|
|
271
|
+
process.on('SIGINT', async () => {
|
|
272
|
+
this.logger.info('MCPServer', 'Received SIGINT, shutting down gracefully...');
|
|
273
|
+
await this.stop();
|
|
274
|
+
process.exit(0);
|
|
275
|
+
});
|
|
276
|
+
process.on('SIGTERM', async () => {
|
|
277
|
+
this.logger.info('MCPServer', 'Received SIGTERM, shutting down gracefully...');
|
|
278
|
+
await this.stop();
|
|
279
|
+
process.exit(0);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
exports.MCPServer = MCPServer;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SessionManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
class SessionManager {
|
|
41
|
+
constructor(logger) {
|
|
42
|
+
this.sessions = new Map();
|
|
43
|
+
this.persistInterval = null;
|
|
44
|
+
this.logger = logger;
|
|
45
|
+
this.sessionFile = path.join(os.homedir(), '.claude-recall', 'sessions.json');
|
|
46
|
+
this.ensureDirectoryExists();
|
|
47
|
+
this.loadSessions();
|
|
48
|
+
// Persist sessions periodically
|
|
49
|
+
this.persistInterval = setInterval(() => {
|
|
50
|
+
this.persistSessions();
|
|
51
|
+
}, 30000); // Every 30 seconds
|
|
52
|
+
}
|
|
53
|
+
ensureDirectoryExists() {
|
|
54
|
+
const dir = path.dirname(this.sessionFile);
|
|
55
|
+
if (!fs.existsSync(dir)) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
createSession(id) {
|
|
60
|
+
const session = {
|
|
61
|
+
id,
|
|
62
|
+
startTime: Date.now(),
|
|
63
|
+
lastActivity: Date.now(),
|
|
64
|
+
toolCalls: 0,
|
|
65
|
+
memories: []
|
|
66
|
+
};
|
|
67
|
+
this.sessions.set(id, session);
|
|
68
|
+
this.persistSessions();
|
|
69
|
+
this.logger.info('SessionManager', 'Session created', { sessionId: id });
|
|
70
|
+
return session;
|
|
71
|
+
}
|
|
72
|
+
getSession(id) {
|
|
73
|
+
return this.sessions.get(id);
|
|
74
|
+
}
|
|
75
|
+
updateSession(id, update) {
|
|
76
|
+
const session = this.sessions.get(id);
|
|
77
|
+
if (session) {
|
|
78
|
+
Object.assign(session, update, { lastActivity: Date.now() });
|
|
79
|
+
this.persistSessions();
|
|
80
|
+
this.logger.debug('SessionManager', 'Session updated', { sessionId: id, update });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
incrementToolCalls(id) {
|
|
84
|
+
const session = this.sessions.get(id);
|
|
85
|
+
if (session) {
|
|
86
|
+
session.toolCalls++;
|
|
87
|
+
session.lastActivity = Date.now();
|
|
88
|
+
this.persistSessions();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
addMemory(id, memoryId) {
|
|
92
|
+
const session = this.sessions.get(id);
|
|
93
|
+
if (session) {
|
|
94
|
+
session.memories.push(memoryId);
|
|
95
|
+
session.lastActivity = Date.now();
|
|
96
|
+
this.persistSessions();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
loadSessions() {
|
|
100
|
+
try {
|
|
101
|
+
if (fs.existsSync(this.sessionFile)) {
|
|
102
|
+
const data = fs.readFileSync(this.sessionFile, 'utf-8');
|
|
103
|
+
const sessions = JSON.parse(data);
|
|
104
|
+
// Convert array back to Map
|
|
105
|
+
if (Array.isArray(sessions)) {
|
|
106
|
+
sessions.forEach(([id, session]) => {
|
|
107
|
+
this.sessions.set(id, session);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
this.logger.info('SessionManager', `Loaded ${this.sessions.size} sessions from disk`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.logger.error('SessionManager', 'Failed to load sessions', error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
persistSessions() {
|
|
118
|
+
try {
|
|
119
|
+
// Save to disk like claude-flow does
|
|
120
|
+
const data = JSON.stringify(Array.from(this.sessions.entries()), null, 2);
|
|
121
|
+
fs.writeFileSync(this.sessionFile, data);
|
|
122
|
+
this.logger.debug('SessionManager', `Persisted ${this.sessions.size} sessions to disk`);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
this.logger.error('SessionManager', 'Failed to persist sessions', error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Clean up old sessions (sessions older than 24 hours with no activity)
|
|
129
|
+
cleanupOldSessions() {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
132
|
+
let removed = 0;
|
|
133
|
+
for (const [id, session] of this.sessions) {
|
|
134
|
+
if (now - session.lastActivity > maxAge) {
|
|
135
|
+
this.sessions.delete(id);
|
|
136
|
+
removed++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (removed > 0) {
|
|
140
|
+
this.logger.info('SessionManager', `Cleaned up ${removed} old sessions`);
|
|
141
|
+
this.persistSessions();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
getAllSessions() {
|
|
145
|
+
return Array.from(this.sessions.values());
|
|
146
|
+
}
|
|
147
|
+
getActiveSessionCount() {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const activeThreshold = 5 * 60 * 1000; // 5 minutes
|
|
150
|
+
return Array.from(this.sessions.values()).filter(session => now - session.lastActivity < activeThreshold).length;
|
|
151
|
+
}
|
|
152
|
+
shutdown() {
|
|
153
|
+
if (this.persistInterval) {
|
|
154
|
+
clearInterval(this.persistInterval);
|
|
155
|
+
this.persistInterval = null;
|
|
156
|
+
}
|
|
157
|
+
this.persistSessions();
|
|
158
|
+
this.logger.info('SessionManager', 'Session manager shut down');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.SessionManager = SessionManager;
|