@joseairosa/recall 1.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/.claude/settings.local.json +17 -0
- package/.env.example +5 -0
- package/.mcp.json +14 -0
- package/.mcp.json.example +14 -0
- package/CHANGELOG.md +189 -0
- package/CLAUDE.md +345 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +149 -0
- package/README.md +612 -0
- package/WORKSPACE_MODES.md +201 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2293 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListResourcesRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
ReadResourceRequestSchema,
|
|
11
|
+
ListPromptsRequestSchema,
|
|
12
|
+
GetPromptRequestSchema
|
|
13
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
|
|
15
|
+
// src/redis/client.ts
|
|
16
|
+
import Redis from "ioredis";
|
|
17
|
+
var redisClient = null;
|
|
18
|
+
function getRedisClient() {
|
|
19
|
+
if (!redisClient) {
|
|
20
|
+
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
21
|
+
redisClient = new Redis(redisUrl, {
|
|
22
|
+
maxRetriesPerRequest: 3,
|
|
23
|
+
retryStrategy(times) {
|
|
24
|
+
const delay = Math.min(times * 50, 2e3);
|
|
25
|
+
return delay;
|
|
26
|
+
},
|
|
27
|
+
reconnectOnError(err) {
|
|
28
|
+
const targetError = "READONLY";
|
|
29
|
+
if (err.message.includes(targetError)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
redisClient.on("error", (err) => {
|
|
36
|
+
console.error("Redis Client Error:", err);
|
|
37
|
+
});
|
|
38
|
+
redisClient.on("connect", () => {
|
|
39
|
+
console.error("Redis Client Connected");
|
|
40
|
+
});
|
|
41
|
+
redisClient.on("ready", () => {
|
|
42
|
+
console.error("Redis Client Ready");
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return redisClient;
|
|
46
|
+
}
|
|
47
|
+
async function closeRedisClient() {
|
|
48
|
+
if (redisClient) {
|
|
49
|
+
await redisClient.quit();
|
|
50
|
+
redisClient = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function checkRedisConnection() {
|
|
54
|
+
try {
|
|
55
|
+
const client = getRedisClient();
|
|
56
|
+
const result = await client.ping();
|
|
57
|
+
return result === "PONG";
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Redis connection check failed:", error);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/tools/index.ts
|
|
65
|
+
import { z as z3 } from "zod";
|
|
66
|
+
import { McpError as McpError2, ErrorCode as ErrorCode2 } from "@modelcontextprotocol/sdk/types.js";
|
|
67
|
+
|
|
68
|
+
// src/redis/memory-store.ts
|
|
69
|
+
import { ulid } from "ulid";
|
|
70
|
+
|
|
71
|
+
// src/embeddings/generator.ts
|
|
72
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
73
|
+
var anthropicClient = null;
|
|
74
|
+
function getAnthropicClient() {
|
|
75
|
+
if (!anthropicClient) {
|
|
76
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
77
|
+
if (!apiKey) {
|
|
78
|
+
throw new Error("ANTHROPIC_API_KEY environment variable is required");
|
|
79
|
+
}
|
|
80
|
+
anthropicClient = new Anthropic({ apiKey });
|
|
81
|
+
}
|
|
82
|
+
return anthropicClient;
|
|
83
|
+
}
|
|
84
|
+
async function generateSemanticFingerprint(text) {
|
|
85
|
+
try {
|
|
86
|
+
const client = getAnthropicClient();
|
|
87
|
+
const response = await client.messages.create({
|
|
88
|
+
model: "claude-3-5-haiku-20241022",
|
|
89
|
+
// Fast, cheap model for this task
|
|
90
|
+
max_tokens: 200,
|
|
91
|
+
messages: [{
|
|
92
|
+
role: "user",
|
|
93
|
+
content: `Extract 5-10 key concepts/keywords from this text. Return ONLY a comma-separated list, no explanations:
|
|
94
|
+
|
|
95
|
+
${text}`
|
|
96
|
+
}]
|
|
97
|
+
});
|
|
98
|
+
const content = response.content[0];
|
|
99
|
+
if (content.type === "text") {
|
|
100
|
+
const keywords = content.text.split(",").map((k) => k.trim().toLowerCase()).filter((k) => k.length > 0);
|
|
101
|
+
return keywords;
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("Error generating semantic fingerprint:", error);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function generateEmbedding(text) {
|
|
110
|
+
try {
|
|
111
|
+
const keywords = await generateSemanticFingerprint(text);
|
|
112
|
+
const vector = createSimpleVector(text, keywords);
|
|
113
|
+
return vector;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("Error generating embedding:", error);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function createSimpleVector(text, keywords) {
|
|
120
|
+
const VECTOR_SIZE = 128;
|
|
121
|
+
const vector = new Array(VECTOR_SIZE).fill(0);
|
|
122
|
+
const normalized = text.toLowerCase();
|
|
123
|
+
const trigrams = extractTrigrams(normalized);
|
|
124
|
+
for (let i = 0; i < Math.min(trigrams.length, 64); i++) {
|
|
125
|
+
const hash = simpleHash(trigrams[i]);
|
|
126
|
+
const index = hash % 64;
|
|
127
|
+
vector[index] += 1;
|
|
128
|
+
}
|
|
129
|
+
for (const keyword of keywords) {
|
|
130
|
+
const hash = simpleHash(keyword);
|
|
131
|
+
const index = 64 + hash % 64;
|
|
132
|
+
vector[index] += 2;
|
|
133
|
+
}
|
|
134
|
+
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
135
|
+
if (magnitude > 0) {
|
|
136
|
+
for (let i = 0; i < vector.length; i++) {
|
|
137
|
+
vector[i] /= magnitude;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return vector;
|
|
141
|
+
}
|
|
142
|
+
function extractTrigrams(text) {
|
|
143
|
+
const trigrams = [];
|
|
144
|
+
for (let i = 0; i < text.length - 2; i++) {
|
|
145
|
+
trigrams.push(text.substring(i, i + 3));
|
|
146
|
+
}
|
|
147
|
+
return trigrams;
|
|
148
|
+
}
|
|
149
|
+
function simpleHash(str) {
|
|
150
|
+
let hash = 0;
|
|
151
|
+
for (let i = 0; i < str.length; i++) {
|
|
152
|
+
const char = str.charCodeAt(i);
|
|
153
|
+
hash = (hash << 5) - hash + char;
|
|
154
|
+
hash = hash & hash;
|
|
155
|
+
}
|
|
156
|
+
return Math.abs(hash);
|
|
157
|
+
}
|
|
158
|
+
function cosineSimilarity(a, b) {
|
|
159
|
+
if (a.length !== b.length) {
|
|
160
|
+
throw new Error("Vectors must have the same length");
|
|
161
|
+
}
|
|
162
|
+
let dotProduct = 0;
|
|
163
|
+
let normA = 0;
|
|
164
|
+
let normB = 0;
|
|
165
|
+
for (let i = 0; i < a.length; i++) {
|
|
166
|
+
dotProduct += a[i] * b[i];
|
|
167
|
+
normA += a[i] * a[i];
|
|
168
|
+
normB += b[i] * b[i];
|
|
169
|
+
}
|
|
170
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/types.ts
|
|
174
|
+
import { z } from "zod";
|
|
175
|
+
var ContextType = z.enum([
|
|
176
|
+
"directive",
|
|
177
|
+
// Instructions or commands
|
|
178
|
+
"information",
|
|
179
|
+
// General facts or knowledge
|
|
180
|
+
"heading",
|
|
181
|
+
// Section headers or organizational markers
|
|
182
|
+
"decision",
|
|
183
|
+
// Decisions made during work
|
|
184
|
+
"code_pattern",
|
|
185
|
+
// Code patterns or conventions
|
|
186
|
+
"requirement",
|
|
187
|
+
// Project requirements
|
|
188
|
+
"error",
|
|
189
|
+
// Error encountered and solution
|
|
190
|
+
"todo",
|
|
191
|
+
// Task or todo item
|
|
192
|
+
"insight",
|
|
193
|
+
// Key insight or realization
|
|
194
|
+
"preference"
|
|
195
|
+
// User preference
|
|
196
|
+
]);
|
|
197
|
+
var MemoryEntrySchema = z.object({
|
|
198
|
+
id: z.string().describe("ULID identifier"),
|
|
199
|
+
timestamp: z.number().describe("Unix timestamp in milliseconds"),
|
|
200
|
+
context_type: ContextType,
|
|
201
|
+
content: z.string().describe("The actual memory content"),
|
|
202
|
+
summary: z.string().optional().describe("Short summary for quick scanning"),
|
|
203
|
+
tags: z.array(z.string()).default([]).describe("Tags for categorization"),
|
|
204
|
+
importance: z.number().min(1).max(10).default(5).describe("Importance score 1-10"),
|
|
205
|
+
session_id: z.string().optional().describe("Optional session grouping"),
|
|
206
|
+
embedding: z.array(z.number()).optional().describe("Vector embedding"),
|
|
207
|
+
ttl_seconds: z.number().optional().describe("Time-to-live in seconds (auto-expires)"),
|
|
208
|
+
expires_at: z.number().optional().describe("Unix timestamp when memory expires")
|
|
209
|
+
});
|
|
210
|
+
var CreateMemorySchema = z.object({
|
|
211
|
+
content: z.string().min(1).describe("The memory content to store"),
|
|
212
|
+
context_type: ContextType.default("information"),
|
|
213
|
+
tags: z.array(z.string()).default([]).describe("Tags for categorization"),
|
|
214
|
+
importance: z.number().min(1).max(10).default(5).describe("Importance score 1-10"),
|
|
215
|
+
summary: z.string().optional().describe("Optional summary"),
|
|
216
|
+
session_id: z.string().optional().describe("Optional session ID"),
|
|
217
|
+
ttl_seconds: z.number().min(60).optional().describe("Time-to-live in seconds (minimum 60s)")
|
|
218
|
+
});
|
|
219
|
+
var BatchCreateMemoriesSchema = z.object({
|
|
220
|
+
memories: z.array(CreateMemorySchema).min(1).describe("Array of memories to store")
|
|
221
|
+
});
|
|
222
|
+
var UpdateMemorySchema = z.object({
|
|
223
|
+
memory_id: z.string().describe("ULID of memory to update"),
|
|
224
|
+
content: z.string().optional(),
|
|
225
|
+
context_type: ContextType.optional(),
|
|
226
|
+
tags: z.array(z.string()).optional(),
|
|
227
|
+
importance: z.number().min(1).max(10).optional(),
|
|
228
|
+
summary: z.string().optional(),
|
|
229
|
+
session_id: z.string().optional()
|
|
230
|
+
});
|
|
231
|
+
var DeleteMemorySchema = z.object({
|
|
232
|
+
memory_id: z.string().describe("ULID of memory to delete")
|
|
233
|
+
});
|
|
234
|
+
var SearchMemorySchema = z.object({
|
|
235
|
+
query: z.string().describe("Search query"),
|
|
236
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of results"),
|
|
237
|
+
min_importance: z.number().min(1).max(10).optional().describe("Filter by minimum importance"),
|
|
238
|
+
context_types: z.array(ContextType).optional().describe("Filter by context types")
|
|
239
|
+
});
|
|
240
|
+
var OrganizeSessionSchema = z.object({
|
|
241
|
+
session_name: z.string().describe("Name for the session"),
|
|
242
|
+
memory_ids: z.array(z.string()).min(1).describe("Array of memory IDs to include"),
|
|
243
|
+
summary: z.string().optional().describe("Optional session summary")
|
|
244
|
+
});
|
|
245
|
+
function createWorkspaceId(path) {
|
|
246
|
+
let hash = 0;
|
|
247
|
+
for (let i = 0; i < path.length; i++) {
|
|
248
|
+
const char = path.charCodeAt(i);
|
|
249
|
+
hash = (hash << 5) - hash + char;
|
|
250
|
+
hash = hash & hash;
|
|
251
|
+
}
|
|
252
|
+
return Math.abs(hash).toString(36);
|
|
253
|
+
}
|
|
254
|
+
var RecallContextSchema = z.object({
|
|
255
|
+
current_task: z.string().describe("Description of what I'm currently working on"),
|
|
256
|
+
query: z.string().optional().describe("Optional specific search query"),
|
|
257
|
+
limit: z.number().min(1).max(20).default(5).describe("Number of results to return"),
|
|
258
|
+
min_importance: z.number().min(1).max(10).default(6).describe("Minimum importance threshold")
|
|
259
|
+
});
|
|
260
|
+
var AnalyzeConversationSchema = z.object({
|
|
261
|
+
conversation_text: z.string().min(1).describe("Conversation text to analyze and extract memories from"),
|
|
262
|
+
auto_categorize: z.boolean().default(true).describe("Automatically categorize extracted memories"),
|
|
263
|
+
auto_store: z.boolean().default(true).describe("Automatically store extracted memories")
|
|
264
|
+
});
|
|
265
|
+
var SummarizeSessionSchema = z.object({
|
|
266
|
+
session_name: z.string().optional().describe("Optional name for the session"),
|
|
267
|
+
auto_create_snapshot: z.boolean().default(true).describe("Automatically create session snapshot"),
|
|
268
|
+
lookback_minutes: z.number().default(60).describe("How many minutes back to look for memories")
|
|
269
|
+
});
|
|
270
|
+
var ExportMemoriesSchema = z.object({
|
|
271
|
+
format: z.enum(["json"]).default("json").describe("Export format"),
|
|
272
|
+
include_embeddings: z.boolean().default(false).describe("Include vector embeddings in export"),
|
|
273
|
+
filter_by_type: z.array(ContextType).optional().describe("Only export specific types"),
|
|
274
|
+
min_importance: z.number().min(1).max(10).optional().describe("Only export above this importance")
|
|
275
|
+
});
|
|
276
|
+
var ImportMemoriesSchema = z.object({
|
|
277
|
+
data: z.string().describe("JSON string of exported memories"),
|
|
278
|
+
overwrite_existing: z.boolean().default(false).describe("Overwrite if memory ID already exists"),
|
|
279
|
+
regenerate_embeddings: z.boolean().default(true).describe("Regenerate embeddings on import")
|
|
280
|
+
});
|
|
281
|
+
var FindDuplicatesSchema = z.object({
|
|
282
|
+
similarity_threshold: z.number().min(0).max(1).default(0.85).describe("Similarity threshold (0-1)"),
|
|
283
|
+
auto_merge: z.boolean().default(false).describe("Automatically merge duplicates"),
|
|
284
|
+
keep_highest_importance: z.boolean().default(true).describe("When merging, keep highest importance")
|
|
285
|
+
});
|
|
286
|
+
var ConsolidateMemoriesSchema = z.object({
|
|
287
|
+
memory_ids: z.array(z.string()).min(2).describe("Array of memory IDs to consolidate"),
|
|
288
|
+
keep_id: z.string().optional().describe("Optional ID of memory to keep (default: highest importance)")
|
|
289
|
+
});
|
|
290
|
+
var RedisKeys = {
|
|
291
|
+
memory: (workspace, id) => `ws:${workspace}:memory:${id}`,
|
|
292
|
+
memories: (workspace) => `ws:${workspace}:memories:all`,
|
|
293
|
+
byType: (workspace, type) => `ws:${workspace}:memories:type:${type}`,
|
|
294
|
+
byTag: (workspace, tag) => `ws:${workspace}:memories:tag:${tag}`,
|
|
295
|
+
timeline: (workspace) => `ws:${workspace}:memories:timeline`,
|
|
296
|
+
session: (workspace, id) => `ws:${workspace}:session:${id}`,
|
|
297
|
+
sessions: (workspace) => `ws:${workspace}:sessions:all`,
|
|
298
|
+
important: (workspace) => `ws:${workspace}:memories:important`
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/redis/memory-store.ts
|
|
302
|
+
var MemoryStore = class {
|
|
303
|
+
redis;
|
|
304
|
+
workspaceId;
|
|
305
|
+
workspacePath;
|
|
306
|
+
constructor(workspacePath) {
|
|
307
|
+
this.redis = getRedisClient();
|
|
308
|
+
this.workspacePath = workspacePath || process.cwd();
|
|
309
|
+
this.workspaceId = createWorkspaceId(this.workspacePath);
|
|
310
|
+
console.error(`[MemoryStore] Workspace: ${this.workspacePath}`);
|
|
311
|
+
console.error(`[MemoryStore] Workspace ID: ${this.workspaceId}`);
|
|
312
|
+
}
|
|
313
|
+
// Store a new memory
|
|
314
|
+
async createMemory(data) {
|
|
315
|
+
const id = ulid();
|
|
316
|
+
const timestamp = Date.now();
|
|
317
|
+
const embedding = await generateEmbedding(data.content);
|
|
318
|
+
const summary = data.summary || this.generateSummary(data.content);
|
|
319
|
+
let expiresAt;
|
|
320
|
+
if (data.ttl_seconds) {
|
|
321
|
+
expiresAt = timestamp + data.ttl_seconds * 1e3;
|
|
322
|
+
}
|
|
323
|
+
const memory = {
|
|
324
|
+
id,
|
|
325
|
+
timestamp,
|
|
326
|
+
context_type: data.context_type,
|
|
327
|
+
content: data.content,
|
|
328
|
+
summary,
|
|
329
|
+
tags: data.tags,
|
|
330
|
+
importance: data.importance,
|
|
331
|
+
session_id: data.session_id,
|
|
332
|
+
embedding,
|
|
333
|
+
ttl_seconds: data.ttl_seconds,
|
|
334
|
+
expires_at: expiresAt
|
|
335
|
+
};
|
|
336
|
+
const pipeline = this.redis.pipeline();
|
|
337
|
+
pipeline.hset(RedisKeys.memory(this.workspaceId, id), this.serializeMemory(memory));
|
|
338
|
+
if (data.ttl_seconds) {
|
|
339
|
+
pipeline.expire(RedisKeys.memory(this.workspaceId, id), data.ttl_seconds);
|
|
340
|
+
}
|
|
341
|
+
pipeline.sadd(RedisKeys.memories(this.workspaceId), id);
|
|
342
|
+
pipeline.zadd(RedisKeys.timeline(this.workspaceId), timestamp, id);
|
|
343
|
+
pipeline.sadd(RedisKeys.byType(this.workspaceId, data.context_type), id);
|
|
344
|
+
for (const tag of data.tags) {
|
|
345
|
+
pipeline.sadd(RedisKeys.byTag(this.workspaceId, tag), id);
|
|
346
|
+
}
|
|
347
|
+
if (data.importance >= 8) {
|
|
348
|
+
pipeline.zadd(RedisKeys.important(this.workspaceId), data.importance, id);
|
|
349
|
+
}
|
|
350
|
+
await pipeline.exec();
|
|
351
|
+
return memory;
|
|
352
|
+
}
|
|
353
|
+
// Batch create memories
|
|
354
|
+
async createMemories(memories) {
|
|
355
|
+
const results = [];
|
|
356
|
+
for (const memoryData of memories) {
|
|
357
|
+
const memory = await this.createMemory(memoryData);
|
|
358
|
+
results.push(memory);
|
|
359
|
+
}
|
|
360
|
+
return results;
|
|
361
|
+
}
|
|
362
|
+
// Get memory by ID
|
|
363
|
+
async getMemory(id) {
|
|
364
|
+
const data = await this.redis.hgetall(RedisKeys.memory(this.workspaceId, id));
|
|
365
|
+
if (!data || Object.keys(data).length === 0) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
return this.deserializeMemory(data);
|
|
369
|
+
}
|
|
370
|
+
// Get multiple memories by IDs
|
|
371
|
+
async getMemories(ids) {
|
|
372
|
+
const memories = [];
|
|
373
|
+
for (const id of ids) {
|
|
374
|
+
const memory = await this.getMemory(id);
|
|
375
|
+
if (memory) {
|
|
376
|
+
memories.push(memory);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return memories;
|
|
380
|
+
}
|
|
381
|
+
// Get recent memories
|
|
382
|
+
async getRecentMemories(limit = 50) {
|
|
383
|
+
const ids = await this.redis.zrevrange(RedisKeys.timeline(this.workspaceId), 0, limit - 1);
|
|
384
|
+
return this.getMemories(ids);
|
|
385
|
+
}
|
|
386
|
+
// Get memories by type
|
|
387
|
+
async getMemoriesByType(type, limit) {
|
|
388
|
+
const ids = await this.redis.smembers(RedisKeys.byType(this.workspaceId, type));
|
|
389
|
+
const memories = await this.getMemories(ids);
|
|
390
|
+
memories.sort((a, b) => b.timestamp - a.timestamp);
|
|
391
|
+
return limit ? memories.slice(0, limit) : memories;
|
|
392
|
+
}
|
|
393
|
+
// Get memories by tag
|
|
394
|
+
async getMemoriesByTag(tag, limit) {
|
|
395
|
+
const ids = await this.redis.smembers(RedisKeys.byTag(this.workspaceId, tag));
|
|
396
|
+
const memories = await this.getMemories(ids);
|
|
397
|
+
memories.sort((a, b) => b.timestamp - a.timestamp);
|
|
398
|
+
return limit ? memories.slice(0, limit) : memories;
|
|
399
|
+
}
|
|
400
|
+
// Get important memories
|
|
401
|
+
async getImportantMemories(minImportance = 8, limit) {
|
|
402
|
+
const results = await this.redis.zrevrangebyscore(
|
|
403
|
+
RedisKeys.important(this.workspaceId),
|
|
404
|
+
10,
|
|
405
|
+
minImportance,
|
|
406
|
+
"LIMIT",
|
|
407
|
+
0,
|
|
408
|
+
limit || 100
|
|
409
|
+
);
|
|
410
|
+
return this.getMemories(results);
|
|
411
|
+
}
|
|
412
|
+
// Update memory
|
|
413
|
+
async updateMemory(id, updates) {
|
|
414
|
+
const existing = await this.getMemory(id);
|
|
415
|
+
if (!existing) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const pipeline = this.redis.pipeline();
|
|
419
|
+
let embedding = existing.embedding;
|
|
420
|
+
if (updates.content && updates.content !== existing.content) {
|
|
421
|
+
embedding = await generateEmbedding(updates.content);
|
|
422
|
+
}
|
|
423
|
+
const updated = {
|
|
424
|
+
...existing,
|
|
425
|
+
...updates,
|
|
426
|
+
embedding,
|
|
427
|
+
summary: updates.summary || (updates.content ? this.generateSummary(updates.content) : existing.summary)
|
|
428
|
+
};
|
|
429
|
+
pipeline.hset(RedisKeys.memory(this.workspaceId, id), this.serializeMemory(updated));
|
|
430
|
+
if (updates.context_type && updates.context_type !== existing.context_type) {
|
|
431
|
+
pipeline.srem(RedisKeys.byType(this.workspaceId, existing.context_type), id);
|
|
432
|
+
pipeline.sadd(RedisKeys.byType(this.workspaceId, updates.context_type), id);
|
|
433
|
+
}
|
|
434
|
+
if (updates.tags) {
|
|
435
|
+
for (const tag of existing.tags) {
|
|
436
|
+
if (!updates.tags.includes(tag)) {
|
|
437
|
+
pipeline.srem(RedisKeys.byTag(this.workspaceId, tag), id);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const tag of updates.tags) {
|
|
441
|
+
if (!existing.tags.includes(tag)) {
|
|
442
|
+
pipeline.sadd(RedisKeys.byTag(this.workspaceId, tag), id);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (updates.importance !== void 0) {
|
|
447
|
+
if (existing.importance >= 8) {
|
|
448
|
+
pipeline.zrem(RedisKeys.important(this.workspaceId), id);
|
|
449
|
+
}
|
|
450
|
+
if (updates.importance >= 8) {
|
|
451
|
+
pipeline.zadd(RedisKeys.important(this.workspaceId), updates.importance, id);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
await pipeline.exec();
|
|
455
|
+
return updated;
|
|
456
|
+
}
|
|
457
|
+
// Delete memory
|
|
458
|
+
async deleteMemory(id) {
|
|
459
|
+
const memory = await this.getMemory(id);
|
|
460
|
+
if (!memory) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
const pipeline = this.redis.pipeline();
|
|
464
|
+
pipeline.del(RedisKeys.memory(this.workspaceId, id));
|
|
465
|
+
pipeline.srem(RedisKeys.memories(this.workspaceId), id);
|
|
466
|
+
pipeline.zrem(RedisKeys.timeline(this.workspaceId), id);
|
|
467
|
+
pipeline.srem(RedisKeys.byType(this.workspaceId, memory.context_type), id);
|
|
468
|
+
for (const tag of memory.tags) {
|
|
469
|
+
pipeline.srem(RedisKeys.byTag(this.workspaceId, tag), id);
|
|
470
|
+
}
|
|
471
|
+
if (memory.importance >= 8) {
|
|
472
|
+
pipeline.zrem(RedisKeys.important(this.workspaceId), id);
|
|
473
|
+
}
|
|
474
|
+
await pipeline.exec();
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
// Semantic search
|
|
478
|
+
async searchMemories(query, limit = 10, minImportance, contextTypes) {
|
|
479
|
+
const queryEmbedding = await generateEmbedding(query);
|
|
480
|
+
let ids;
|
|
481
|
+
if (contextTypes && contextTypes.length > 0) {
|
|
482
|
+
const sets = contextTypes.map((type) => RedisKeys.byType(this.workspaceId, type));
|
|
483
|
+
ids = await this.redis.sunion(...sets);
|
|
484
|
+
} else {
|
|
485
|
+
ids = await this.redis.smembers(RedisKeys.memories(this.workspaceId));
|
|
486
|
+
}
|
|
487
|
+
const memories = await this.getMemories(ids);
|
|
488
|
+
let filtered = memories;
|
|
489
|
+
if (minImportance !== void 0) {
|
|
490
|
+
filtered = memories.filter((m) => m.importance >= minImportance);
|
|
491
|
+
}
|
|
492
|
+
const withSimilarity = filtered.map((memory) => ({
|
|
493
|
+
...memory,
|
|
494
|
+
similarity: memory.embedding ? cosineSimilarity(queryEmbedding, memory.embedding) : 0
|
|
495
|
+
}));
|
|
496
|
+
withSimilarity.sort((a, b) => b.similarity - a.similarity);
|
|
497
|
+
return withSimilarity.slice(0, limit);
|
|
498
|
+
}
|
|
499
|
+
// Create session
|
|
500
|
+
async createSession(name, memoryIds, summary) {
|
|
501
|
+
const sessionId = ulid();
|
|
502
|
+
const timestamp = Date.now();
|
|
503
|
+
const validIds = [];
|
|
504
|
+
for (const id of memoryIds) {
|
|
505
|
+
const exists = await this.redis.exists(RedisKeys.memory(this.workspaceId, id));
|
|
506
|
+
if (exists) {
|
|
507
|
+
validIds.push(id);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const session = {
|
|
511
|
+
session_id: sessionId,
|
|
512
|
+
session_name: name,
|
|
513
|
+
created_at: timestamp,
|
|
514
|
+
memory_count: validIds.length,
|
|
515
|
+
summary,
|
|
516
|
+
memory_ids: validIds
|
|
517
|
+
};
|
|
518
|
+
await this.redis.hset(RedisKeys.session(this.workspaceId, sessionId), {
|
|
519
|
+
session_id: sessionId,
|
|
520
|
+
session_name: name,
|
|
521
|
+
created_at: timestamp.toString(),
|
|
522
|
+
memory_count: validIds.length.toString(),
|
|
523
|
+
summary: summary || "",
|
|
524
|
+
memory_ids: JSON.stringify(validIds)
|
|
525
|
+
});
|
|
526
|
+
await this.redis.sadd(RedisKeys.sessions(this.workspaceId), sessionId);
|
|
527
|
+
return session;
|
|
528
|
+
}
|
|
529
|
+
// Get session
|
|
530
|
+
async getSession(sessionId) {
|
|
531
|
+
const data = await this.redis.hgetall(RedisKeys.session(this.workspaceId, sessionId));
|
|
532
|
+
if (!data || Object.keys(data).length === 0) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
session_id: data.session_id,
|
|
537
|
+
session_name: data.session_name,
|
|
538
|
+
created_at: parseInt(data.created_at, 10),
|
|
539
|
+
memory_count: parseInt(data.memory_count, 10),
|
|
540
|
+
summary: data.summary || void 0,
|
|
541
|
+
memory_ids: JSON.parse(data.memory_ids)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
// Get all sessions
|
|
545
|
+
async getAllSessions() {
|
|
546
|
+
const ids = await this.redis.smembers(RedisKeys.sessions(this.workspaceId));
|
|
547
|
+
const sessions = [];
|
|
548
|
+
for (const id of ids) {
|
|
549
|
+
const session = await this.getSession(id);
|
|
550
|
+
if (session) {
|
|
551
|
+
sessions.push(session);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return sessions.sort((a, b) => b.created_at - a.created_at);
|
|
555
|
+
}
|
|
556
|
+
// Get memories in session
|
|
557
|
+
async getSessionMemories(sessionId) {
|
|
558
|
+
const session = await this.getSession(sessionId);
|
|
559
|
+
if (!session) {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
return this.getMemories(session.memory_ids);
|
|
563
|
+
}
|
|
564
|
+
// Generate summary stats
|
|
565
|
+
async getSummaryStats() {
|
|
566
|
+
const totalMemories = await this.redis.scard(RedisKeys.memories(this.workspaceId));
|
|
567
|
+
const totalSessions = await this.redis.scard(RedisKeys.sessions(this.workspaceId));
|
|
568
|
+
const importantCount = await this.redis.zcard(RedisKeys.important(this.workspaceId));
|
|
569
|
+
const byType = {};
|
|
570
|
+
const types = ["directive", "information", "heading", "decision", "code_pattern", "requirement", "error", "todo", "insight", "preference"];
|
|
571
|
+
for (const type of types) {
|
|
572
|
+
byType[type] = await this.redis.scard(RedisKeys.byType(this.workspaceId, type));
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
total_memories: totalMemories,
|
|
576
|
+
by_type: byType,
|
|
577
|
+
total_sessions: totalSessions,
|
|
578
|
+
important_count: importantCount,
|
|
579
|
+
workspace_path: this.workspacePath
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
// Merge multiple memories into one
|
|
583
|
+
async mergeMemories(memoryIds, keepId) {
|
|
584
|
+
const memories = await this.getMemories(memoryIds);
|
|
585
|
+
if (memories.length === 0) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
const toKeep = keepId ? memories.find((m) => m.id === keepId) : memories.reduce(
|
|
589
|
+
(prev, current) => current.importance > prev.importance ? current : prev
|
|
590
|
+
);
|
|
591
|
+
if (!toKeep) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const allTags = /* @__PURE__ */ new Set();
|
|
595
|
+
const contentParts = [];
|
|
596
|
+
for (const memory of memories) {
|
|
597
|
+
if (memory.id !== toKeep.id) {
|
|
598
|
+
contentParts.push(memory.content);
|
|
599
|
+
}
|
|
600
|
+
memory.tags.forEach((tag) => allTags.add(tag));
|
|
601
|
+
}
|
|
602
|
+
const mergedContent = contentParts.length > 0 ? `${toKeep.content}
|
|
603
|
+
|
|
604
|
+
--- Merged content ---
|
|
605
|
+
${contentParts.join("\n\n")}` : toKeep.content;
|
|
606
|
+
const updated = await this.updateMemory(toKeep.id, {
|
|
607
|
+
content: mergedContent,
|
|
608
|
+
tags: Array.from(allTags),
|
|
609
|
+
importance: Math.max(...memories.map((m) => m.importance))
|
|
610
|
+
});
|
|
611
|
+
for (const memory of memories) {
|
|
612
|
+
if (memory.id !== toKeep.id) {
|
|
613
|
+
await this.deleteMemory(memory.id);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return updated;
|
|
617
|
+
}
|
|
618
|
+
// Helper: Generate summary from content (first 100 chars)
|
|
619
|
+
generateSummary(content) {
|
|
620
|
+
return content.length > 100 ? content.substring(0, 100) + "..." : content;
|
|
621
|
+
}
|
|
622
|
+
// Helper: Serialize memory for Redis
|
|
623
|
+
serializeMemory(memory) {
|
|
624
|
+
return {
|
|
625
|
+
id: memory.id,
|
|
626
|
+
timestamp: memory.timestamp.toString(),
|
|
627
|
+
context_type: memory.context_type,
|
|
628
|
+
content: memory.content,
|
|
629
|
+
summary: memory.summary || "",
|
|
630
|
+
tags: JSON.stringify(memory.tags),
|
|
631
|
+
importance: memory.importance.toString(),
|
|
632
|
+
session_id: memory.session_id || "",
|
|
633
|
+
embedding: JSON.stringify(memory.embedding || []),
|
|
634
|
+
ttl_seconds: memory.ttl_seconds?.toString() || "",
|
|
635
|
+
expires_at: memory.expires_at?.toString() || ""
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
// Helper: Deserialize memory from Redis
|
|
639
|
+
deserializeMemory(data) {
|
|
640
|
+
return {
|
|
641
|
+
id: data.id,
|
|
642
|
+
timestamp: parseInt(data.timestamp, 10),
|
|
643
|
+
context_type: data.context_type,
|
|
644
|
+
content: data.content,
|
|
645
|
+
summary: data.summary || void 0,
|
|
646
|
+
tags: JSON.parse(data.tags || "[]"),
|
|
647
|
+
importance: parseInt(data.importance, 10),
|
|
648
|
+
session_id: data.session_id || void 0,
|
|
649
|
+
embedding: JSON.parse(data.embedding || "[]"),
|
|
650
|
+
ttl_seconds: data.ttl_seconds ? parseInt(data.ttl_seconds, 10) : void 0,
|
|
651
|
+
expires_at: data.expires_at ? parseInt(data.expires_at, 10) : void 0
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// src/tools/context-tools.ts
|
|
657
|
+
import { z as z2 } from "zod";
|
|
658
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
659
|
+
|
|
660
|
+
// src/analysis/conversation-analyzer.ts
|
|
661
|
+
import Anthropic2 from "@anthropic-ai/sdk";
|
|
662
|
+
var ConversationAnalyzer = class {
|
|
663
|
+
client;
|
|
664
|
+
constructor() {
|
|
665
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
666
|
+
if (!apiKey) {
|
|
667
|
+
throw new Error("ANTHROPIC_API_KEY environment variable is required");
|
|
668
|
+
}
|
|
669
|
+
this.client = new Anthropic2({ apiKey });
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Analyze conversation and extract structured memories
|
|
673
|
+
*/
|
|
674
|
+
async analyzeConversation(conversationText) {
|
|
675
|
+
try {
|
|
676
|
+
const response = await this.client.messages.create({
|
|
677
|
+
model: "claude-3-5-haiku-20241022",
|
|
678
|
+
max_tokens: 2e3,
|
|
679
|
+
messages: [{
|
|
680
|
+
role: "user",
|
|
681
|
+
content: `Analyze this conversation and extract important information that should be remembered long-term.
|
|
682
|
+
|
|
683
|
+
For each important piece of information, output EXACTLY in this JSON format (one per line):
|
|
684
|
+
{"content":"the information","type":"directive|information|decision|code_pattern|requirement|error|todo|insight|preference","importance":1-10,"tags":["tag1","tag2"],"summary":"brief summary"}
|
|
685
|
+
|
|
686
|
+
Guidelines:
|
|
687
|
+
- Extract directives (instructions to follow)
|
|
688
|
+
- Extract decisions (choices made)
|
|
689
|
+
- Extract code_patterns (coding conventions)
|
|
690
|
+
- Extract requirements (project specs)
|
|
691
|
+
- Extract errors (problems and solutions)
|
|
692
|
+
- Extract insights (key realizations)
|
|
693
|
+
- Extract preferences (user preferences)
|
|
694
|
+
- Importance: 10=critical, 8-9=very important, 6-7=important, 1-5=nice to have
|
|
695
|
+
- Tags: relevant keywords for categorization
|
|
696
|
+
- Summary: max 50 chars
|
|
697
|
+
|
|
698
|
+
Conversation:
|
|
699
|
+
${conversationText}
|
|
700
|
+
|
|
701
|
+
Output ONLY the JSON objects, one per line, no other text:`
|
|
702
|
+
}]
|
|
703
|
+
});
|
|
704
|
+
const content = response.content[0];
|
|
705
|
+
if (content.type !== "text") {
|
|
706
|
+
return [];
|
|
707
|
+
}
|
|
708
|
+
const lines = content.text.split("\n").filter((line) => line.trim().startsWith("{"));
|
|
709
|
+
const extracted = [];
|
|
710
|
+
for (const line of lines) {
|
|
711
|
+
try {
|
|
712
|
+
const parsed = JSON.parse(line.trim());
|
|
713
|
+
if (parsed.content && parsed.type && parsed.importance) {
|
|
714
|
+
extracted.push({
|
|
715
|
+
content: parsed.content,
|
|
716
|
+
context_type: this.normalizeContextType(parsed.type),
|
|
717
|
+
importance: Math.min(10, Math.max(1, parseInt(parsed.importance))),
|
|
718
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags : [],
|
|
719
|
+
summary: parsed.summary || void 0
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
} catch (e) {
|
|
723
|
+
console.error("Failed to parse line:", line, e);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return extracted;
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.error("Error analyzing conversation:", error);
|
|
729
|
+
throw error;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Generate a summary of a session
|
|
734
|
+
*/
|
|
735
|
+
async summarizeSession(memories) {
|
|
736
|
+
try {
|
|
737
|
+
const memoriesText = memories.sort((a, b) => b.importance - a.importance).map((m) => `[${m.context_type}] ${m.content}`).join("\n");
|
|
738
|
+
const response = await this.client.messages.create({
|
|
739
|
+
model: "claude-3-5-haiku-20241022",
|
|
740
|
+
max_tokens: 500,
|
|
741
|
+
messages: [{
|
|
742
|
+
role: "user",
|
|
743
|
+
content: `Summarize this work session in 2-3 sentences. Focus on what was accomplished, decided, or learned.
|
|
744
|
+
|
|
745
|
+
Session memories:
|
|
746
|
+
${memoriesText}
|
|
747
|
+
|
|
748
|
+
Summary (2-3 sentences):`
|
|
749
|
+
}]
|
|
750
|
+
});
|
|
751
|
+
const content = response.content[0];
|
|
752
|
+
if (content.type === "text") {
|
|
753
|
+
return content.text.trim();
|
|
754
|
+
}
|
|
755
|
+
return "Session completed with multiple activities";
|
|
756
|
+
} catch (error) {
|
|
757
|
+
console.error("Error summarizing session:", error);
|
|
758
|
+
return "Session summary unavailable";
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Enhance a search query for better semantic matching
|
|
763
|
+
*/
|
|
764
|
+
async enhanceQuery(currentTask, query) {
|
|
765
|
+
const combined = query ? `${currentTask} ${query}` : currentTask;
|
|
766
|
+
return combined;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Normalize context type strings from Claude
|
|
770
|
+
*/
|
|
771
|
+
normalizeContextType(type) {
|
|
772
|
+
const normalized = type.toLowerCase().trim();
|
|
773
|
+
const mapping = {
|
|
774
|
+
"directive": "directive",
|
|
775
|
+
"instruction": "directive",
|
|
776
|
+
"command": "directive",
|
|
777
|
+
"information": "information",
|
|
778
|
+
"info": "information",
|
|
779
|
+
"fact": "information",
|
|
780
|
+
"heading": "heading",
|
|
781
|
+
"section": "heading",
|
|
782
|
+
"title": "heading",
|
|
783
|
+
"decision": "decision",
|
|
784
|
+
"choice": "decision",
|
|
785
|
+
"code_pattern": "code_pattern",
|
|
786
|
+
"pattern": "code_pattern",
|
|
787
|
+
"convention": "code_pattern",
|
|
788
|
+
"requirement": "requirement",
|
|
789
|
+
"req": "requirement",
|
|
790
|
+
"spec": "requirement",
|
|
791
|
+
"error": "error",
|
|
792
|
+
"bug": "error",
|
|
793
|
+
"issue": "error",
|
|
794
|
+
"todo": "todo",
|
|
795
|
+
"task": "todo",
|
|
796
|
+
"insight": "insight",
|
|
797
|
+
"realization": "insight",
|
|
798
|
+
"learning": "insight",
|
|
799
|
+
"preference": "preference",
|
|
800
|
+
"pref": "preference",
|
|
801
|
+
"setting": "preference"
|
|
802
|
+
};
|
|
803
|
+
return mapping[normalized] || "information";
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// src/tools/context-tools.ts
|
|
808
|
+
var memoryStore = new MemoryStore();
|
|
809
|
+
var analyzer = new ConversationAnalyzer();
|
|
810
|
+
var recall_relevant_context = {
|
|
811
|
+
description: "Proactively search memory for context relevant to current task. Use this when you need to recall patterns, decisions, or conventions.",
|
|
812
|
+
inputSchema: zodToJsonSchema(RecallContextSchema),
|
|
813
|
+
handler: async (args) => {
|
|
814
|
+
try {
|
|
815
|
+
const enhancedQuery = await analyzer.enhanceQuery(args.current_task, args.query);
|
|
816
|
+
const results = await memoryStore.searchMemories(
|
|
817
|
+
enhancedQuery,
|
|
818
|
+
args.limit,
|
|
819
|
+
args.min_importance
|
|
820
|
+
);
|
|
821
|
+
const formattedResults = results.map((r) => ({
|
|
822
|
+
content: r.content,
|
|
823
|
+
summary: r.summary,
|
|
824
|
+
context_type: r.context_type,
|
|
825
|
+
importance: r.importance,
|
|
826
|
+
tags: r.tags,
|
|
827
|
+
similarity: Math.round(r.similarity * 100) / 100
|
|
828
|
+
}));
|
|
829
|
+
return {
|
|
830
|
+
content: [
|
|
831
|
+
{
|
|
832
|
+
type: "text",
|
|
833
|
+
text: JSON.stringify({
|
|
834
|
+
current_task: args.current_task,
|
|
835
|
+
found: results.length,
|
|
836
|
+
relevant_memories: formattedResults
|
|
837
|
+
}, null, 2)
|
|
838
|
+
}
|
|
839
|
+
]
|
|
840
|
+
};
|
|
841
|
+
} catch (error) {
|
|
842
|
+
throw new McpError(
|
|
843
|
+
ErrorCode.InternalError,
|
|
844
|
+
`Failed to recall context: ${error instanceof Error ? error.message : String(error)}`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
var analyze_and_remember = {
|
|
850
|
+
description: "Analyze conversation text and automatically extract and store important information (decisions, patterns, directives, etc.). Use this after important discussions.",
|
|
851
|
+
inputSchema: zodToJsonSchema(AnalyzeConversationSchema),
|
|
852
|
+
handler: async (args) => {
|
|
853
|
+
try {
|
|
854
|
+
const extracted = await analyzer.analyzeConversation(args.conversation_text);
|
|
855
|
+
const result = {
|
|
856
|
+
extracted_memories: extracted,
|
|
857
|
+
total_count: extracted.length
|
|
858
|
+
};
|
|
859
|
+
if (args.auto_store && extracted.length > 0) {
|
|
860
|
+
const memories = await memoryStore.createMemories(
|
|
861
|
+
extracted.map((e) => ({
|
|
862
|
+
content: e.content,
|
|
863
|
+
context_type: e.context_type,
|
|
864
|
+
importance: e.importance,
|
|
865
|
+
tags: e.tags,
|
|
866
|
+
summary: e.summary
|
|
867
|
+
}))
|
|
868
|
+
);
|
|
869
|
+
result.stored_ids = memories.map((m) => m.id);
|
|
870
|
+
}
|
|
871
|
+
const response = {
|
|
872
|
+
success: true,
|
|
873
|
+
analyzed: result.total_count,
|
|
874
|
+
stored: result.stored_ids?.length || 0,
|
|
875
|
+
breakdown: {
|
|
876
|
+
directives: extracted.filter((e) => e.context_type === "directive").length,
|
|
877
|
+
decisions: extracted.filter((e) => e.context_type === "decision").length,
|
|
878
|
+
patterns: extracted.filter((e) => e.context_type === "code_pattern").length,
|
|
879
|
+
requirements: extracted.filter((e) => e.context_type === "requirement").length,
|
|
880
|
+
errors: extracted.filter((e) => e.context_type === "error").length,
|
|
881
|
+
insights: extracted.filter((e) => e.context_type === "insight").length,
|
|
882
|
+
other: extracted.filter((e) => !["directive", "decision", "code_pattern", "requirement", "error", "insight"].includes(e.context_type)).length
|
|
883
|
+
},
|
|
884
|
+
memories: extracted.map((e) => ({
|
|
885
|
+
content: e.content.substring(0, 100) + (e.content.length > 100 ? "..." : ""),
|
|
886
|
+
type: e.context_type,
|
|
887
|
+
importance: e.importance
|
|
888
|
+
}))
|
|
889
|
+
};
|
|
890
|
+
return {
|
|
891
|
+
content: [
|
|
892
|
+
{
|
|
893
|
+
type: "text",
|
|
894
|
+
text: JSON.stringify(response, null, 2)
|
|
895
|
+
}
|
|
896
|
+
]
|
|
897
|
+
};
|
|
898
|
+
} catch (error) {
|
|
899
|
+
throw new McpError(
|
|
900
|
+
ErrorCode.InternalError,
|
|
901
|
+
`Failed to analyze conversation: ${error instanceof Error ? error.message : String(error)}`
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
var summarize_session = {
|
|
907
|
+
description: "Summarize the current work session and create a snapshot. Use this at the end of a work session to preserve context.",
|
|
908
|
+
inputSchema: zodToJsonSchema(SummarizeSessionSchema),
|
|
909
|
+
handler: async (args) => {
|
|
910
|
+
try {
|
|
911
|
+
const lookbackMs = args.lookback_minutes * 60 * 1e3;
|
|
912
|
+
const cutoffTime = Date.now() - lookbackMs;
|
|
913
|
+
const allRecent = await memoryStore.getRecentMemories(100);
|
|
914
|
+
const sessionMemories = allRecent.filter((m) => m.timestamp >= cutoffTime);
|
|
915
|
+
if (sessionMemories.length === 0) {
|
|
916
|
+
return {
|
|
917
|
+
content: [
|
|
918
|
+
{
|
|
919
|
+
type: "text",
|
|
920
|
+
text: JSON.stringify({
|
|
921
|
+
success: false,
|
|
922
|
+
message: "No memories found in the specified lookback period",
|
|
923
|
+
lookback_minutes: args.lookback_minutes
|
|
924
|
+
}, null, 2)
|
|
925
|
+
}
|
|
926
|
+
]
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
const summary = await analyzer.summarizeSession(
|
|
930
|
+
sessionMemories.map((m) => ({
|
|
931
|
+
content: m.content,
|
|
932
|
+
context_type: m.context_type,
|
|
933
|
+
importance: m.importance
|
|
934
|
+
}))
|
|
935
|
+
);
|
|
936
|
+
let sessionInfo = null;
|
|
937
|
+
if (args.auto_create_snapshot) {
|
|
938
|
+
const sessionName = args.session_name || `Session ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
939
|
+
sessionInfo = await memoryStore.createSession(
|
|
940
|
+
sessionName,
|
|
941
|
+
sessionMemories.map((m) => m.id),
|
|
942
|
+
summary
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
content: [
|
|
947
|
+
{
|
|
948
|
+
type: "text",
|
|
949
|
+
text: JSON.stringify({
|
|
950
|
+
success: true,
|
|
951
|
+
summary,
|
|
952
|
+
session_id: sessionInfo?.session_id,
|
|
953
|
+
session_name: sessionInfo?.session_name,
|
|
954
|
+
memory_count: sessionMemories.length,
|
|
955
|
+
lookback_minutes: args.lookback_minutes,
|
|
956
|
+
breakdown: {
|
|
957
|
+
directives: sessionMemories.filter((m) => m.context_type === "directive").length,
|
|
958
|
+
decisions: sessionMemories.filter((m) => m.context_type === "decision").length,
|
|
959
|
+
patterns: sessionMemories.filter((m) => m.context_type === "code_pattern").length,
|
|
960
|
+
insights: sessionMemories.filter((m) => m.context_type === "insight").length
|
|
961
|
+
}
|
|
962
|
+
}, null, 2)
|
|
963
|
+
}
|
|
964
|
+
]
|
|
965
|
+
};
|
|
966
|
+
} catch (error) {
|
|
967
|
+
throw new McpError(
|
|
968
|
+
ErrorCode.InternalError,
|
|
969
|
+
`Failed to summarize session: ${error instanceof Error ? error.message : String(error)}`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
function zodToJsonSchema(schema) {
|
|
975
|
+
if (schema instanceof z2.ZodObject) {
|
|
976
|
+
const shape = schema._def.shape();
|
|
977
|
+
const properties = {};
|
|
978
|
+
const required = [];
|
|
979
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
980
|
+
properties[key] = zodToJsonSchemaInner(value);
|
|
981
|
+
if (!value.isOptional()) {
|
|
982
|
+
required.push(key);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
type: "object",
|
|
987
|
+
properties,
|
|
988
|
+
required
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
return zodToJsonSchemaInner(schema);
|
|
992
|
+
}
|
|
993
|
+
function zodToJsonSchemaInner(schema) {
|
|
994
|
+
if (schema instanceof z2.ZodString) {
|
|
995
|
+
const result = { type: "string" };
|
|
996
|
+
if (schema.description) result.description = schema.description;
|
|
997
|
+
return result;
|
|
998
|
+
}
|
|
999
|
+
if (schema instanceof z2.ZodNumber) {
|
|
1000
|
+
const result = { type: "number" };
|
|
1001
|
+
if (schema.description) result.description = schema.description;
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
if (schema instanceof z2.ZodBoolean) {
|
|
1005
|
+
const result = { type: "boolean" };
|
|
1006
|
+
if (schema.description) result.description = schema.description;
|
|
1007
|
+
return result;
|
|
1008
|
+
}
|
|
1009
|
+
if (schema instanceof z2.ZodArray) {
|
|
1010
|
+
const result = {
|
|
1011
|
+
type: "array",
|
|
1012
|
+
items: zodToJsonSchemaInner(schema.element)
|
|
1013
|
+
};
|
|
1014
|
+
if (schema.description) result.description = schema.description;
|
|
1015
|
+
return result;
|
|
1016
|
+
}
|
|
1017
|
+
if (schema instanceof z2.ZodEnum) {
|
|
1018
|
+
const result = {
|
|
1019
|
+
type: "string",
|
|
1020
|
+
enum: schema.options
|
|
1021
|
+
};
|
|
1022
|
+
if (schema.description) result.description = schema.description;
|
|
1023
|
+
return result;
|
|
1024
|
+
}
|
|
1025
|
+
if (schema instanceof z2.ZodOptional) {
|
|
1026
|
+
return zodToJsonSchemaInner(schema.unwrap());
|
|
1027
|
+
}
|
|
1028
|
+
if (schema instanceof z2.ZodDefault) {
|
|
1029
|
+
const inner = zodToJsonSchemaInner(schema._def.innerType);
|
|
1030
|
+
inner.default = schema._def.defaultValue();
|
|
1031
|
+
return inner;
|
|
1032
|
+
}
|
|
1033
|
+
if (schema instanceof z2.ZodObject) {
|
|
1034
|
+
return zodToJsonSchema(schema);
|
|
1035
|
+
}
|
|
1036
|
+
return { type: "string" };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/tools/export-import-tools.ts
|
|
1040
|
+
async function exportMemories(args, workspacePath) {
|
|
1041
|
+
const store = new MemoryStore(workspacePath);
|
|
1042
|
+
let memories;
|
|
1043
|
+
if (args.filter_by_type && args.filter_by_type.length > 0) {
|
|
1044
|
+
const memoriesByType = [];
|
|
1045
|
+
for (const type of args.filter_by_type) {
|
|
1046
|
+
const typeMemories = await store.getMemoriesByType(type);
|
|
1047
|
+
memoriesByType.push(...typeMemories);
|
|
1048
|
+
}
|
|
1049
|
+
const uniqueMap = /* @__PURE__ */ new Map();
|
|
1050
|
+
for (const memory of memoriesByType) {
|
|
1051
|
+
uniqueMap.set(memory.id, memory);
|
|
1052
|
+
}
|
|
1053
|
+
memories = Array.from(uniqueMap.values());
|
|
1054
|
+
} else {
|
|
1055
|
+
memories = await store.getRecentMemories(1e4);
|
|
1056
|
+
}
|
|
1057
|
+
if (args.min_importance !== void 0) {
|
|
1058
|
+
memories = memories.filter((m) => m.importance >= args.min_importance);
|
|
1059
|
+
}
|
|
1060
|
+
const exportData = memories.map((memory) => {
|
|
1061
|
+
if (!args.include_embeddings) {
|
|
1062
|
+
const { embedding, ...rest } = memory;
|
|
1063
|
+
return rest;
|
|
1064
|
+
}
|
|
1065
|
+
return memory;
|
|
1066
|
+
});
|
|
1067
|
+
const exportObject = {
|
|
1068
|
+
version: "1.2.0",
|
|
1069
|
+
exported_at: Date.now(),
|
|
1070
|
+
memory_count: exportData.length,
|
|
1071
|
+
memories: exportData
|
|
1072
|
+
};
|
|
1073
|
+
const jsonString = JSON.stringify(exportObject, null, 2);
|
|
1074
|
+
return {
|
|
1075
|
+
content: [
|
|
1076
|
+
{
|
|
1077
|
+
type: "text",
|
|
1078
|
+
text: `Successfully exported ${exportData.length} memories
|
|
1079
|
+
|
|
1080
|
+
${jsonString}`
|
|
1081
|
+
}
|
|
1082
|
+
]
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function importMemories(args, workspacePath) {
|
|
1086
|
+
const store = new MemoryStore(workspacePath);
|
|
1087
|
+
let importData;
|
|
1088
|
+
try {
|
|
1089
|
+
importData = JSON.parse(args.data);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
throw new Error(`Invalid JSON data: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1092
|
+
}
|
|
1093
|
+
if (!importData.memories || !Array.isArray(importData.memories)) {
|
|
1094
|
+
throw new Error("Invalid import format: missing memories array");
|
|
1095
|
+
}
|
|
1096
|
+
const results = {
|
|
1097
|
+
imported: 0,
|
|
1098
|
+
skipped: 0,
|
|
1099
|
+
overwritten: 0,
|
|
1100
|
+
errors: []
|
|
1101
|
+
};
|
|
1102
|
+
for (const memoryData of importData.memories) {
|
|
1103
|
+
try {
|
|
1104
|
+
const existing = await store.getMemory(memoryData.id);
|
|
1105
|
+
if (existing && !args.overwrite_existing) {
|
|
1106
|
+
results.skipped++;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
const createData = {
|
|
1110
|
+
content: memoryData.content,
|
|
1111
|
+
context_type: memoryData.context_type,
|
|
1112
|
+
tags: memoryData.tags || [],
|
|
1113
|
+
importance: memoryData.importance || 5,
|
|
1114
|
+
summary: memoryData.summary,
|
|
1115
|
+
session_id: memoryData.session_id,
|
|
1116
|
+
ttl_seconds: memoryData.ttl_seconds
|
|
1117
|
+
};
|
|
1118
|
+
if (existing && args.overwrite_existing) {
|
|
1119
|
+
await store.updateMemory(memoryData.id, createData);
|
|
1120
|
+
results.overwritten++;
|
|
1121
|
+
} else {
|
|
1122
|
+
const importedMemory = {
|
|
1123
|
+
id: memoryData.id,
|
|
1124
|
+
timestamp: memoryData.timestamp || Date.now(),
|
|
1125
|
+
context_type: memoryData.context_type,
|
|
1126
|
+
content: memoryData.content,
|
|
1127
|
+
summary: memoryData.summary,
|
|
1128
|
+
tags: memoryData.tags || [],
|
|
1129
|
+
importance: memoryData.importance || 5,
|
|
1130
|
+
session_id: memoryData.session_id,
|
|
1131
|
+
embedding: args.regenerate_embeddings ? void 0 : memoryData.embedding,
|
|
1132
|
+
ttl_seconds: memoryData.ttl_seconds,
|
|
1133
|
+
expires_at: memoryData.expires_at
|
|
1134
|
+
};
|
|
1135
|
+
if (args.regenerate_embeddings) {
|
|
1136
|
+
await store.createMemory(createData);
|
|
1137
|
+
} else {
|
|
1138
|
+
await store.createMemory(createData);
|
|
1139
|
+
}
|
|
1140
|
+
results.imported++;
|
|
1141
|
+
}
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
results.errors.push(`Failed to import memory ${memoryData.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const summary = [
|
|
1147
|
+
`Import completed:`,
|
|
1148
|
+
`- Imported: ${results.imported}`,
|
|
1149
|
+
`- Overwritten: ${results.overwritten}`,
|
|
1150
|
+
`- Skipped: ${results.skipped}`,
|
|
1151
|
+
`- Errors: ${results.errors.length}`
|
|
1152
|
+
];
|
|
1153
|
+
if (results.errors.length > 0) {
|
|
1154
|
+
summary.push("", "Errors:", ...results.errors.slice(0, 10));
|
|
1155
|
+
if (results.errors.length > 10) {
|
|
1156
|
+
summary.push(`... and ${results.errors.length - 10} more errors`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
content: [
|
|
1161
|
+
{
|
|
1162
|
+
type: "text",
|
|
1163
|
+
text: summary.join("\n")
|
|
1164
|
+
}
|
|
1165
|
+
]
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
async function findDuplicates(args, workspacePath) {
|
|
1169
|
+
const store = new MemoryStore(workspacePath);
|
|
1170
|
+
const memories = await store.getRecentMemories(1e4);
|
|
1171
|
+
const duplicateGroups = [];
|
|
1172
|
+
const processed = /* @__PURE__ */ new Set();
|
|
1173
|
+
for (let i = 0; i < memories.length; i++) {
|
|
1174
|
+
const memory1 = memories[i];
|
|
1175
|
+
if (processed.has(memory1.id)) {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
const similarMemories = [memory1];
|
|
1179
|
+
let maxSimilarity = 0;
|
|
1180
|
+
for (let j = i + 1; j < memories.length; j++) {
|
|
1181
|
+
const memory2 = memories[j];
|
|
1182
|
+
if (processed.has(memory2.id)) {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (memory1.embedding && memory2.embedding) {
|
|
1186
|
+
const similarity = cosineSimilarity(memory1.embedding, memory2.embedding);
|
|
1187
|
+
if (similarity >= args.similarity_threshold) {
|
|
1188
|
+
similarMemories.push(memory2);
|
|
1189
|
+
maxSimilarity = Math.max(maxSimilarity, similarity);
|
|
1190
|
+
processed.add(memory2.id);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (similarMemories.length > 1) {
|
|
1195
|
+
duplicateGroups.push({
|
|
1196
|
+
memories: similarMemories,
|
|
1197
|
+
similarity_score: maxSimilarity
|
|
1198
|
+
});
|
|
1199
|
+
processed.add(memory1.id);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (args.auto_merge && duplicateGroups.length > 0) {
|
|
1203
|
+
let mergedCount = 0;
|
|
1204
|
+
for (const group of duplicateGroups) {
|
|
1205
|
+
try {
|
|
1206
|
+
const toKeep = args.keep_highest_importance ? group.memories.reduce(
|
|
1207
|
+
(prev, current) => current.importance > prev.importance ? current : prev
|
|
1208
|
+
) : group.memories[0];
|
|
1209
|
+
const allTags = /* @__PURE__ */ new Set();
|
|
1210
|
+
for (const memory of group.memories) {
|
|
1211
|
+
memory.tags.forEach((tag) => allTags.add(tag));
|
|
1212
|
+
}
|
|
1213
|
+
await store.updateMemory(toKeep.id, {
|
|
1214
|
+
tags: Array.from(allTags)
|
|
1215
|
+
});
|
|
1216
|
+
for (const memory of group.memories) {
|
|
1217
|
+
if (memory.id !== toKeep.id) {
|
|
1218
|
+
await store.deleteMemory(memory.id);
|
|
1219
|
+
mergedCount++;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
console.error(`Failed to merge duplicate group: ${error}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
content: [
|
|
1228
|
+
{
|
|
1229
|
+
type: "text",
|
|
1230
|
+
text: `Found ${duplicateGroups.length} duplicate groups and merged ${mergedCount} duplicate memories.`
|
|
1231
|
+
}
|
|
1232
|
+
]
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
if (duplicateGroups.length === 0) {
|
|
1236
|
+
return {
|
|
1237
|
+
content: [
|
|
1238
|
+
{
|
|
1239
|
+
type: "text",
|
|
1240
|
+
text: "No duplicate memories found."
|
|
1241
|
+
}
|
|
1242
|
+
]
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
const report = [
|
|
1246
|
+
`Found ${duplicateGroups.length} duplicate groups:
|
|
1247
|
+
`
|
|
1248
|
+
];
|
|
1249
|
+
for (let i = 0; i < duplicateGroups.length; i++) {
|
|
1250
|
+
const group = duplicateGroups[i];
|
|
1251
|
+
report.push(`Group ${i + 1} (similarity: ${group.similarity_score.toFixed(3)}):`);
|
|
1252
|
+
for (const memory of group.memories) {
|
|
1253
|
+
report.push(` - ID: ${memory.id} | Importance: ${memory.importance} | Summary: ${memory.summary || memory.content.substring(0, 50)}`);
|
|
1254
|
+
}
|
|
1255
|
+
report.push("");
|
|
1256
|
+
}
|
|
1257
|
+
return {
|
|
1258
|
+
content: [
|
|
1259
|
+
{
|
|
1260
|
+
type: "text",
|
|
1261
|
+
text: report.join("\n")
|
|
1262
|
+
}
|
|
1263
|
+
]
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async function consolidateMemories(args, workspacePath) {
|
|
1267
|
+
const store = new MemoryStore(workspacePath);
|
|
1268
|
+
const result = await store.mergeMemories(args.memory_ids, args.keep_id);
|
|
1269
|
+
if (!result) {
|
|
1270
|
+
return {
|
|
1271
|
+
content: [
|
|
1272
|
+
{
|
|
1273
|
+
type: "text",
|
|
1274
|
+
text: "Failed to consolidate memories. Check that all memory IDs exist."
|
|
1275
|
+
}
|
|
1276
|
+
]
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
return {
|
|
1280
|
+
content: [
|
|
1281
|
+
{
|
|
1282
|
+
type: "text",
|
|
1283
|
+
text: `Successfully consolidated ${args.memory_ids.length} memories into ID: ${result.id}
|
|
1284
|
+
|
|
1285
|
+
Merged Memory:
|
|
1286
|
+
- Content: ${result.summary || result.content.substring(0, 100)}
|
|
1287
|
+
- Tags: ${result.tags.join(", ")}
|
|
1288
|
+
- Importance: ${result.importance}`
|
|
1289
|
+
}
|
|
1290
|
+
]
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/tools/index.ts
|
|
1295
|
+
var memoryStore2 = new MemoryStore();
|
|
1296
|
+
var tools = {
|
|
1297
|
+
// Context management tools
|
|
1298
|
+
recall_relevant_context,
|
|
1299
|
+
analyze_and_remember,
|
|
1300
|
+
summarize_session,
|
|
1301
|
+
// Export/Import tools
|
|
1302
|
+
export_memories: {
|
|
1303
|
+
description: "Export memories to JSON format with optional filtering",
|
|
1304
|
+
inputSchema: zodToJsonSchema2(ExportMemoriesSchema),
|
|
1305
|
+
handler: async (args) => {
|
|
1306
|
+
try {
|
|
1307
|
+
return await exportMemories(args);
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
throw new McpError2(
|
|
1310
|
+
ErrorCode2.InternalError,
|
|
1311
|
+
`Failed to export memories: ${error instanceof Error ? error.message : String(error)}`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
import_memories: {
|
|
1317
|
+
description: "Import memories from JSON export data",
|
|
1318
|
+
inputSchema: zodToJsonSchema2(ImportMemoriesSchema),
|
|
1319
|
+
handler: async (args) => {
|
|
1320
|
+
try {
|
|
1321
|
+
return await importMemories(args);
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
throw new McpError2(
|
|
1324
|
+
ErrorCode2.InternalError,
|
|
1325
|
+
`Failed to import memories: ${error instanceof Error ? error.message : String(error)}`
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1330
|
+
find_duplicates: {
|
|
1331
|
+
description: "Find and optionally merge duplicate memories based on similarity",
|
|
1332
|
+
inputSchema: zodToJsonSchema2(FindDuplicatesSchema),
|
|
1333
|
+
handler: async (args) => {
|
|
1334
|
+
try {
|
|
1335
|
+
return await findDuplicates(args);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
throw new McpError2(
|
|
1338
|
+
ErrorCode2.InternalError,
|
|
1339
|
+
`Failed to find duplicates: ${error instanceof Error ? error.message : String(error)}`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
},
|
|
1344
|
+
consolidate_memories: {
|
|
1345
|
+
description: "Manually consolidate multiple memories into one",
|
|
1346
|
+
inputSchema: zodToJsonSchema2(ConsolidateMemoriesSchema),
|
|
1347
|
+
handler: async (args) => {
|
|
1348
|
+
try {
|
|
1349
|
+
return await consolidateMemories(args);
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
throw new McpError2(
|
|
1352
|
+
ErrorCode2.InternalError,
|
|
1353
|
+
`Failed to consolidate memories: ${error instanceof Error ? error.message : String(error)}`
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
},
|
|
1358
|
+
// Original memory tools
|
|
1359
|
+
store_memory: {
|
|
1360
|
+
description: "Store a new memory/context entry for long-term persistence",
|
|
1361
|
+
inputSchema: zodToJsonSchema2(CreateMemorySchema),
|
|
1362
|
+
handler: async (args) => {
|
|
1363
|
+
try {
|
|
1364
|
+
const memory = await memoryStore2.createMemory(args);
|
|
1365
|
+
return {
|
|
1366
|
+
content: [
|
|
1367
|
+
{
|
|
1368
|
+
type: "text",
|
|
1369
|
+
text: JSON.stringify({
|
|
1370
|
+
success: true,
|
|
1371
|
+
memory_id: memory.id,
|
|
1372
|
+
timestamp: memory.timestamp,
|
|
1373
|
+
summary: memory.summary
|
|
1374
|
+
}, null, 2)
|
|
1375
|
+
}
|
|
1376
|
+
]
|
|
1377
|
+
};
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
throw new McpError2(
|
|
1380
|
+
ErrorCode2.InternalError,
|
|
1381
|
+
`Failed to store memory: ${error instanceof Error ? error.message : String(error)}`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1386
|
+
store_batch_memories: {
|
|
1387
|
+
description: "Store multiple memories in a batch operation",
|
|
1388
|
+
inputSchema: zodToJsonSchema2(BatchCreateMemoriesSchema),
|
|
1389
|
+
handler: async (args) => {
|
|
1390
|
+
try {
|
|
1391
|
+
const memories = await memoryStore2.createMemories(args.memories);
|
|
1392
|
+
return {
|
|
1393
|
+
content: [
|
|
1394
|
+
{
|
|
1395
|
+
type: "text",
|
|
1396
|
+
text: JSON.stringify({
|
|
1397
|
+
success: true,
|
|
1398
|
+
count: memories.length,
|
|
1399
|
+
memory_ids: memories.map((m) => m.id)
|
|
1400
|
+
}, null, 2)
|
|
1401
|
+
}
|
|
1402
|
+
]
|
|
1403
|
+
};
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
throw new McpError2(
|
|
1406
|
+
ErrorCode2.InternalError,
|
|
1407
|
+
`Failed to store memories: ${error instanceof Error ? error.message : String(error)}`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
},
|
|
1412
|
+
update_memory: {
|
|
1413
|
+
description: "Update an existing memory entry",
|
|
1414
|
+
inputSchema: zodToJsonSchema2(UpdateMemorySchema),
|
|
1415
|
+
handler: async (args) => {
|
|
1416
|
+
try {
|
|
1417
|
+
const { memory_id, ...updates } = args;
|
|
1418
|
+
const memory = await memoryStore2.updateMemory(memory_id, updates);
|
|
1419
|
+
if (!memory) {
|
|
1420
|
+
throw new McpError2(ErrorCode2.InvalidRequest, `Memory ${memory_id} not found`);
|
|
1421
|
+
}
|
|
1422
|
+
return {
|
|
1423
|
+
content: [
|
|
1424
|
+
{
|
|
1425
|
+
type: "text",
|
|
1426
|
+
text: JSON.stringify({
|
|
1427
|
+
success: true,
|
|
1428
|
+
memory_id: memory.id,
|
|
1429
|
+
updated_at: Date.now()
|
|
1430
|
+
}, null, 2)
|
|
1431
|
+
}
|
|
1432
|
+
]
|
|
1433
|
+
};
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
if (error instanceof McpError2) throw error;
|
|
1436
|
+
throw new McpError2(
|
|
1437
|
+
ErrorCode2.InternalError,
|
|
1438
|
+
`Failed to update memory: ${error instanceof Error ? error.message : String(error)}`
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
delete_memory: {
|
|
1444
|
+
description: "Delete a memory entry",
|
|
1445
|
+
inputSchema: zodToJsonSchema2(DeleteMemorySchema),
|
|
1446
|
+
handler: async (args) => {
|
|
1447
|
+
try {
|
|
1448
|
+
const success = await memoryStore2.deleteMemory(args.memory_id);
|
|
1449
|
+
if (!success) {
|
|
1450
|
+
throw new McpError2(ErrorCode2.InvalidRequest, `Memory ${args.memory_id} not found`);
|
|
1451
|
+
}
|
|
1452
|
+
return {
|
|
1453
|
+
content: [
|
|
1454
|
+
{
|
|
1455
|
+
type: "text",
|
|
1456
|
+
text: JSON.stringify({
|
|
1457
|
+
success: true,
|
|
1458
|
+
memory_id: args.memory_id,
|
|
1459
|
+
deleted_at: Date.now()
|
|
1460
|
+
}, null, 2)
|
|
1461
|
+
}
|
|
1462
|
+
]
|
|
1463
|
+
};
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
if (error instanceof McpError2) throw error;
|
|
1466
|
+
throw new McpError2(
|
|
1467
|
+
ErrorCode2.InternalError,
|
|
1468
|
+
`Failed to delete memory: ${error instanceof Error ? error.message : String(error)}`
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
search_memories: {
|
|
1474
|
+
description: "Search memories using semantic similarity",
|
|
1475
|
+
inputSchema: zodToJsonSchema2(SearchMemorySchema),
|
|
1476
|
+
handler: async (args) => {
|
|
1477
|
+
try {
|
|
1478
|
+
const results = await memoryStore2.searchMemories(
|
|
1479
|
+
args.query,
|
|
1480
|
+
args.limit,
|
|
1481
|
+
args.min_importance,
|
|
1482
|
+
args.context_types
|
|
1483
|
+
);
|
|
1484
|
+
return {
|
|
1485
|
+
content: [
|
|
1486
|
+
{
|
|
1487
|
+
type: "text",
|
|
1488
|
+
text: JSON.stringify({
|
|
1489
|
+
query: args.query,
|
|
1490
|
+
count: results.length,
|
|
1491
|
+
results: results.map((r) => ({
|
|
1492
|
+
memory_id: r.id,
|
|
1493
|
+
content: r.content,
|
|
1494
|
+
summary: r.summary,
|
|
1495
|
+
context_type: r.context_type,
|
|
1496
|
+
importance: r.importance,
|
|
1497
|
+
tags: r.tags,
|
|
1498
|
+
similarity: r.similarity,
|
|
1499
|
+
timestamp: r.timestamp
|
|
1500
|
+
}))
|
|
1501
|
+
}, null, 2)
|
|
1502
|
+
}
|
|
1503
|
+
]
|
|
1504
|
+
};
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
throw new McpError2(
|
|
1507
|
+
ErrorCode2.InternalError,
|
|
1508
|
+
`Failed to search memories: ${error instanceof Error ? error.message : String(error)}`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
},
|
|
1513
|
+
organize_session: {
|
|
1514
|
+
description: "Create a session snapshot grouping related memories",
|
|
1515
|
+
inputSchema: zodToJsonSchema2(OrganizeSessionSchema),
|
|
1516
|
+
handler: async (args) => {
|
|
1517
|
+
try {
|
|
1518
|
+
const session = await memoryStore2.createSession(
|
|
1519
|
+
args.session_name,
|
|
1520
|
+
args.memory_ids,
|
|
1521
|
+
args.summary
|
|
1522
|
+
);
|
|
1523
|
+
return {
|
|
1524
|
+
content: [
|
|
1525
|
+
{
|
|
1526
|
+
type: "text",
|
|
1527
|
+
text: JSON.stringify({
|
|
1528
|
+
success: true,
|
|
1529
|
+
session_id: session.session_id,
|
|
1530
|
+
session_name: session.session_name,
|
|
1531
|
+
memory_count: session.memory_count,
|
|
1532
|
+
created_at: session.created_at
|
|
1533
|
+
}, null, 2)
|
|
1534
|
+
}
|
|
1535
|
+
]
|
|
1536
|
+
};
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
throw new McpError2(
|
|
1539
|
+
ErrorCode2.InternalError,
|
|
1540
|
+
`Failed to organize session: ${error instanceof Error ? error.message : String(error)}`
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
function zodToJsonSchema2(schema) {
|
|
1547
|
+
if (schema instanceof z3.ZodObject) {
|
|
1548
|
+
const shape = schema._def.shape();
|
|
1549
|
+
const properties = {};
|
|
1550
|
+
const required = [];
|
|
1551
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
1552
|
+
properties[key] = zodToJsonSchemaInner2(value);
|
|
1553
|
+
if (!value.isOptional()) {
|
|
1554
|
+
required.push(key);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return {
|
|
1558
|
+
type: "object",
|
|
1559
|
+
properties,
|
|
1560
|
+
required
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
return zodToJsonSchemaInner2(schema);
|
|
1564
|
+
}
|
|
1565
|
+
function zodToJsonSchemaInner2(schema) {
|
|
1566
|
+
if (schema instanceof z3.ZodString) {
|
|
1567
|
+
const result = { type: "string" };
|
|
1568
|
+
if (schema.description) result.description = schema.description;
|
|
1569
|
+
return result;
|
|
1570
|
+
}
|
|
1571
|
+
if (schema instanceof z3.ZodNumber) {
|
|
1572
|
+
const result = { type: "number" };
|
|
1573
|
+
if (schema.description) result.description = schema.description;
|
|
1574
|
+
return result;
|
|
1575
|
+
}
|
|
1576
|
+
if (schema instanceof z3.ZodBoolean) {
|
|
1577
|
+
const result = { type: "boolean" };
|
|
1578
|
+
if (schema.description) result.description = schema.description;
|
|
1579
|
+
return result;
|
|
1580
|
+
}
|
|
1581
|
+
if (schema instanceof z3.ZodArray) {
|
|
1582
|
+
const result = {
|
|
1583
|
+
type: "array",
|
|
1584
|
+
items: zodToJsonSchemaInner2(schema.element)
|
|
1585
|
+
};
|
|
1586
|
+
if (schema.description) result.description = schema.description;
|
|
1587
|
+
return result;
|
|
1588
|
+
}
|
|
1589
|
+
if (schema instanceof z3.ZodEnum) {
|
|
1590
|
+
const result = {
|
|
1591
|
+
type: "string",
|
|
1592
|
+
enum: schema.options
|
|
1593
|
+
};
|
|
1594
|
+
if (schema.description) result.description = schema.description;
|
|
1595
|
+
return result;
|
|
1596
|
+
}
|
|
1597
|
+
if (schema instanceof z3.ZodOptional) {
|
|
1598
|
+
return zodToJsonSchemaInner2(schema.unwrap());
|
|
1599
|
+
}
|
|
1600
|
+
if (schema instanceof z3.ZodDefault) {
|
|
1601
|
+
const inner = zodToJsonSchemaInner2(schema._def.innerType);
|
|
1602
|
+
inner.default = schema._def.defaultValue();
|
|
1603
|
+
return inner;
|
|
1604
|
+
}
|
|
1605
|
+
if (schema instanceof z3.ZodObject) {
|
|
1606
|
+
return zodToJsonSchema2(schema);
|
|
1607
|
+
}
|
|
1608
|
+
return { type: "string" };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/resources/index.ts
|
|
1612
|
+
import { McpError as McpError3, ErrorCode as ErrorCode3 } from "@modelcontextprotocol/sdk/types.js";
|
|
1613
|
+
|
|
1614
|
+
// src/resources/analytics.ts
|
|
1615
|
+
async function getAnalytics(workspacePath) {
|
|
1616
|
+
const store = new MemoryStore(workspacePath);
|
|
1617
|
+
const stats = await store.getSummaryStats();
|
|
1618
|
+
const recentMemories = await store.getRecentMemories(1e3);
|
|
1619
|
+
const now = Date.now();
|
|
1620
|
+
const day24h = now - 24 * 60 * 60 * 1e3;
|
|
1621
|
+
const day7d = now - 7 * 24 * 60 * 60 * 1e3;
|
|
1622
|
+
const day30d = now - 30 * 24 * 60 * 60 * 1e3;
|
|
1623
|
+
const memories24h = recentMemories.filter((m) => m.timestamp >= day24h);
|
|
1624
|
+
const memories7d = recentMemories.filter((m) => m.timestamp >= day7d);
|
|
1625
|
+
const memories30d = recentMemories.filter((m) => m.timestamp >= day30d);
|
|
1626
|
+
const typeCount24h = /* @__PURE__ */ new Map();
|
|
1627
|
+
for (const memory of memories24h) {
|
|
1628
|
+
typeCount24h.set(memory.context_type, (typeCount24h.get(memory.context_type) || 0) + 1);
|
|
1629
|
+
}
|
|
1630
|
+
const mostActiveTypes24h = Array.from(typeCount24h.entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count).slice(0, 5);
|
|
1631
|
+
const tagCount = /* @__PURE__ */ new Map();
|
|
1632
|
+
for (const memory of recentMemories) {
|
|
1633
|
+
for (const tag of memory.tags) {
|
|
1634
|
+
tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const topTags = Array.from(tagCount.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1638
|
+
const importanceDist = {
|
|
1639
|
+
critical: recentMemories.filter((m) => m.importance >= 9).length,
|
|
1640
|
+
high: recentMemories.filter((m) => m.importance >= 7 && m.importance < 9).length,
|
|
1641
|
+
medium: recentMemories.filter((m) => m.importance >= 5 && m.importance < 7).length,
|
|
1642
|
+
low: recentMemories.filter((m) => m.importance < 5).length
|
|
1643
|
+
};
|
|
1644
|
+
const activityByDay = /* @__PURE__ */ new Map();
|
|
1645
|
+
const last7Days = [];
|
|
1646
|
+
for (let i = 6; i >= 0; i--) {
|
|
1647
|
+
const date = new Date(now - i * 24 * 60 * 60 * 1e3);
|
|
1648
|
+
const dateStr = date.toISOString().split("T")[0];
|
|
1649
|
+
last7Days.push(dateStr);
|
|
1650
|
+
activityByDay.set(dateStr, { count: 0, types: /* @__PURE__ */ new Map() });
|
|
1651
|
+
}
|
|
1652
|
+
for (const memory of memories7d) {
|
|
1653
|
+
const dateStr = new Date(memory.timestamp).toISOString().split("T")[0];
|
|
1654
|
+
const activity = activityByDay.get(dateStr);
|
|
1655
|
+
if (activity) {
|
|
1656
|
+
activity.count++;
|
|
1657
|
+
activity.types.set(memory.context_type, (activity.types.get(memory.context_type) || 0) + 1);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const recentActivity = last7Days.map((date) => {
|
|
1661
|
+
const activity = activityByDay.get(date);
|
|
1662
|
+
return {
|
|
1663
|
+
date,
|
|
1664
|
+
count: activity.count,
|
|
1665
|
+
types: Object.fromEntries(activity.types)
|
|
1666
|
+
};
|
|
1667
|
+
});
|
|
1668
|
+
const analytics = {
|
|
1669
|
+
overview: stats,
|
|
1670
|
+
trends: {
|
|
1671
|
+
memories_last_24h: memories24h.length,
|
|
1672
|
+
memories_last_7d: memories7d.length,
|
|
1673
|
+
memories_last_30d: memories30d.length,
|
|
1674
|
+
most_active_types_24h: mostActiveTypes24h
|
|
1675
|
+
},
|
|
1676
|
+
top_tags: topTags,
|
|
1677
|
+
importance_distribution: importanceDist,
|
|
1678
|
+
recent_activity: recentActivity
|
|
1679
|
+
};
|
|
1680
|
+
return formatAnalytics(analytics);
|
|
1681
|
+
}
|
|
1682
|
+
function formatAnalytics(data) {
|
|
1683
|
+
const lines = [
|
|
1684
|
+
"# Memory Analytics Dashboard",
|
|
1685
|
+
"",
|
|
1686
|
+
`**Workspace**: ${data.overview.workspace_path}`,
|
|
1687
|
+
"",
|
|
1688
|
+
"## Overview",
|
|
1689
|
+
`- Total Memories: ${data.overview.total_memories}`,
|
|
1690
|
+
`- Sessions: ${data.overview.total_sessions}`,
|
|
1691
|
+
`- Important Memories (\u22658): ${data.overview.important_count}`,
|
|
1692
|
+
"",
|
|
1693
|
+
"### Memories by Type"
|
|
1694
|
+
];
|
|
1695
|
+
for (const [type, count] of Object.entries(data.overview.by_type)) {
|
|
1696
|
+
if (count > 0) {
|
|
1697
|
+
lines.push(`- ${type}: ${count}`);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
lines.push("", "## Recent Activity Trends");
|
|
1701
|
+
lines.push(`- Last 24 hours: ${data.trends.memories_last_24h} memories`);
|
|
1702
|
+
lines.push(`- Last 7 days: ${data.trends.memories_last_7d} memories`);
|
|
1703
|
+
lines.push(`- Last 30 days: ${data.trends.memories_last_30d} memories`);
|
|
1704
|
+
if (data.trends.most_active_types_24h.length > 0) {
|
|
1705
|
+
lines.push("", "### Most Active Types (24h)");
|
|
1706
|
+
for (const { type, count } of data.trends.most_active_types_24h) {
|
|
1707
|
+
lines.push(`- ${type}: ${count}`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (data.top_tags.length > 0) {
|
|
1711
|
+
lines.push("", "## Top Tags");
|
|
1712
|
+
for (const { tag, count } of data.top_tags) {
|
|
1713
|
+
lines.push(`- ${tag}: ${count}`);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
lines.push("", "## Importance Distribution");
|
|
1717
|
+
lines.push(`- Critical (9-10): ${data.importance_distribution.critical}`);
|
|
1718
|
+
lines.push(`- High (7-8): ${data.importance_distribution.high}`);
|
|
1719
|
+
lines.push(`- Medium (5-6): ${data.importance_distribution.medium}`);
|
|
1720
|
+
lines.push(`- Low (1-4): ${data.importance_distribution.low}`);
|
|
1721
|
+
lines.push("", "## Activity Last 7 Days");
|
|
1722
|
+
for (const activity of data.recent_activity) {
|
|
1723
|
+
const typeSummary = Object.entries(activity.types).map(([type, count]) => `${type}:${count}`).join(", ");
|
|
1724
|
+
lines.push(`- ${activity.date}: ${activity.count} memories ${typeSummary ? `(${typeSummary})` : ""}`);
|
|
1725
|
+
}
|
|
1726
|
+
return lines.join("\n");
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/resources/index.ts
|
|
1730
|
+
var memoryStore3 = new MemoryStore();
|
|
1731
|
+
var resources = {
|
|
1732
|
+
"memory://recent": {
|
|
1733
|
+
name: "Recent Memories",
|
|
1734
|
+
description: "Get the most recent memories (default: 50)",
|
|
1735
|
+
mimeType: "application/json",
|
|
1736
|
+
handler: async (uri) => {
|
|
1737
|
+
const limit = parseInt(uri.searchParams.get("limit") || "50", 10);
|
|
1738
|
+
const memories = await memoryStore3.getRecentMemories(limit);
|
|
1739
|
+
return {
|
|
1740
|
+
contents: [
|
|
1741
|
+
{
|
|
1742
|
+
uri: uri.toString(),
|
|
1743
|
+
mimeType: "application/json",
|
|
1744
|
+
text: JSON.stringify(
|
|
1745
|
+
{
|
|
1746
|
+
count: memories.length,
|
|
1747
|
+
memories: memories.map((m) => ({
|
|
1748
|
+
memory_id: m.id,
|
|
1749
|
+
content: m.content,
|
|
1750
|
+
summary: m.summary,
|
|
1751
|
+
context_type: m.context_type,
|
|
1752
|
+
importance: m.importance,
|
|
1753
|
+
tags: m.tags,
|
|
1754
|
+
timestamp: m.timestamp,
|
|
1755
|
+
session_id: m.session_id
|
|
1756
|
+
}))
|
|
1757
|
+
},
|
|
1758
|
+
null,
|
|
1759
|
+
2
|
|
1760
|
+
)
|
|
1761
|
+
}
|
|
1762
|
+
]
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
"memory://by-type/{type}": {
|
|
1767
|
+
name: "Memories by Type",
|
|
1768
|
+
description: "Get memories filtered by context type",
|
|
1769
|
+
mimeType: "application/json",
|
|
1770
|
+
handler: async (uri, params) => {
|
|
1771
|
+
const type = params.type;
|
|
1772
|
+
const limit = uri.searchParams.get("limit") ? parseInt(uri.searchParams.get("limit"), 10) : void 0;
|
|
1773
|
+
const memories = await memoryStore3.getMemoriesByType(type, limit);
|
|
1774
|
+
return {
|
|
1775
|
+
contents: [
|
|
1776
|
+
{
|
|
1777
|
+
uri: uri.toString(),
|
|
1778
|
+
mimeType: "application/json",
|
|
1779
|
+
text: JSON.stringify(
|
|
1780
|
+
{
|
|
1781
|
+
context_type: type,
|
|
1782
|
+
count: memories.length,
|
|
1783
|
+
memories: memories.map((m) => ({
|
|
1784
|
+
memory_id: m.id,
|
|
1785
|
+
content: m.content,
|
|
1786
|
+
summary: m.summary,
|
|
1787
|
+
importance: m.importance,
|
|
1788
|
+
tags: m.tags,
|
|
1789
|
+
timestamp: m.timestamp
|
|
1790
|
+
}))
|
|
1791
|
+
},
|
|
1792
|
+
null,
|
|
1793
|
+
2
|
|
1794
|
+
)
|
|
1795
|
+
}
|
|
1796
|
+
]
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
"memory://by-tag/{tag}": {
|
|
1801
|
+
name: "Memories by Tag",
|
|
1802
|
+
description: "Get memories filtered by tag",
|
|
1803
|
+
mimeType: "application/json",
|
|
1804
|
+
handler: async (uri, params) => {
|
|
1805
|
+
const { tag } = params;
|
|
1806
|
+
const limit = uri.searchParams.get("limit") ? parseInt(uri.searchParams.get("limit"), 10) : void 0;
|
|
1807
|
+
const memories = await memoryStore3.getMemoriesByTag(tag, limit);
|
|
1808
|
+
return {
|
|
1809
|
+
contents: [
|
|
1810
|
+
{
|
|
1811
|
+
uri: uri.toString(),
|
|
1812
|
+
mimeType: "application/json",
|
|
1813
|
+
text: JSON.stringify(
|
|
1814
|
+
{
|
|
1815
|
+
tag,
|
|
1816
|
+
count: memories.length,
|
|
1817
|
+
memories: memories.map((m) => ({
|
|
1818
|
+
memory_id: m.id,
|
|
1819
|
+
content: m.content,
|
|
1820
|
+
summary: m.summary,
|
|
1821
|
+
context_type: m.context_type,
|
|
1822
|
+
importance: m.importance,
|
|
1823
|
+
tags: m.tags,
|
|
1824
|
+
timestamp: m.timestamp
|
|
1825
|
+
}))
|
|
1826
|
+
},
|
|
1827
|
+
null,
|
|
1828
|
+
2
|
|
1829
|
+
)
|
|
1830
|
+
}
|
|
1831
|
+
]
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
},
|
|
1835
|
+
"memory://important": {
|
|
1836
|
+
name: "Important Memories",
|
|
1837
|
+
description: "Get high-importance memories (importance >= 8)",
|
|
1838
|
+
mimeType: "application/json",
|
|
1839
|
+
handler: async (uri) => {
|
|
1840
|
+
const minImportance = parseInt(uri.searchParams.get("min") || "8", 10);
|
|
1841
|
+
const limit = uri.searchParams.get("limit") ? parseInt(uri.searchParams.get("limit"), 10) : void 0;
|
|
1842
|
+
const memories = await memoryStore3.getImportantMemories(minImportance, limit);
|
|
1843
|
+
return {
|
|
1844
|
+
contents: [
|
|
1845
|
+
{
|
|
1846
|
+
uri: uri.toString(),
|
|
1847
|
+
mimeType: "application/json",
|
|
1848
|
+
text: JSON.stringify(
|
|
1849
|
+
{
|
|
1850
|
+
min_importance: minImportance,
|
|
1851
|
+
count: memories.length,
|
|
1852
|
+
memories: memories.map((m) => ({
|
|
1853
|
+
memory_id: m.id,
|
|
1854
|
+
content: m.content,
|
|
1855
|
+
summary: m.summary,
|
|
1856
|
+
context_type: m.context_type,
|
|
1857
|
+
importance: m.importance,
|
|
1858
|
+
tags: m.tags,
|
|
1859
|
+
timestamp: m.timestamp
|
|
1860
|
+
}))
|
|
1861
|
+
},
|
|
1862
|
+
null,
|
|
1863
|
+
2
|
|
1864
|
+
)
|
|
1865
|
+
}
|
|
1866
|
+
]
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
},
|
|
1870
|
+
"memory://session/{session_id}": {
|
|
1871
|
+
name: "Session Memories",
|
|
1872
|
+
description: "Get all memories in a specific session",
|
|
1873
|
+
mimeType: "application/json",
|
|
1874
|
+
handler: async (uri, params) => {
|
|
1875
|
+
const { session_id } = params;
|
|
1876
|
+
const session = await memoryStore3.getSession(session_id);
|
|
1877
|
+
if (!session) {
|
|
1878
|
+
throw new McpError3(ErrorCode3.InvalidRequest, `Session ${session_id} not found`);
|
|
1879
|
+
}
|
|
1880
|
+
const memories = await memoryStore3.getSessionMemories(session_id);
|
|
1881
|
+
return {
|
|
1882
|
+
contents: [
|
|
1883
|
+
{
|
|
1884
|
+
uri: uri.toString(),
|
|
1885
|
+
mimeType: "application/json",
|
|
1886
|
+
text: JSON.stringify(
|
|
1887
|
+
{
|
|
1888
|
+
session_id: session.session_id,
|
|
1889
|
+
session_name: session.session_name,
|
|
1890
|
+
created_at: session.created_at,
|
|
1891
|
+
summary: session.summary,
|
|
1892
|
+
count: memories.length,
|
|
1893
|
+
memories: memories.map((m) => ({
|
|
1894
|
+
memory_id: m.id,
|
|
1895
|
+
content: m.content,
|
|
1896
|
+
summary: m.summary,
|
|
1897
|
+
context_type: m.context_type,
|
|
1898
|
+
importance: m.importance,
|
|
1899
|
+
tags: m.tags,
|
|
1900
|
+
timestamp: m.timestamp
|
|
1901
|
+
}))
|
|
1902
|
+
},
|
|
1903
|
+
null,
|
|
1904
|
+
2
|
|
1905
|
+
)
|
|
1906
|
+
}
|
|
1907
|
+
]
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
"memory://sessions": {
|
|
1912
|
+
name: "All Sessions",
|
|
1913
|
+
description: "Get list of all sessions",
|
|
1914
|
+
mimeType: "application/json",
|
|
1915
|
+
handler: async (uri) => {
|
|
1916
|
+
const sessions = await memoryStore3.getAllSessions();
|
|
1917
|
+
return {
|
|
1918
|
+
contents: [
|
|
1919
|
+
{
|
|
1920
|
+
uri: uri.toString(),
|
|
1921
|
+
mimeType: "application/json",
|
|
1922
|
+
text: JSON.stringify(
|
|
1923
|
+
{
|
|
1924
|
+
count: sessions.length,
|
|
1925
|
+
sessions: sessions.map((s) => ({
|
|
1926
|
+
session_id: s.session_id,
|
|
1927
|
+
session_name: s.session_name,
|
|
1928
|
+
created_at: s.created_at,
|
|
1929
|
+
memory_count: s.memory_count,
|
|
1930
|
+
summary: s.summary
|
|
1931
|
+
}))
|
|
1932
|
+
},
|
|
1933
|
+
null,
|
|
1934
|
+
2
|
|
1935
|
+
)
|
|
1936
|
+
}
|
|
1937
|
+
]
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
},
|
|
1941
|
+
"memory://summary": {
|
|
1942
|
+
name: "Memory Summary",
|
|
1943
|
+
description: "Get overall summary statistics of stored memories",
|
|
1944
|
+
mimeType: "application/json",
|
|
1945
|
+
handler: async (uri) => {
|
|
1946
|
+
const stats = await memoryStore3.getSummaryStats();
|
|
1947
|
+
return {
|
|
1948
|
+
contents: [
|
|
1949
|
+
{
|
|
1950
|
+
uri: uri.toString(),
|
|
1951
|
+
mimeType: "application/json",
|
|
1952
|
+
text: JSON.stringify(stats, null, 2)
|
|
1953
|
+
}
|
|
1954
|
+
]
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
},
|
|
1958
|
+
"memory://search": {
|
|
1959
|
+
name: "Search Memories",
|
|
1960
|
+
description: "Search memories using semantic similarity",
|
|
1961
|
+
mimeType: "application/json",
|
|
1962
|
+
handler: async (uri) => {
|
|
1963
|
+
const query = uri.searchParams.get("q");
|
|
1964
|
+
if (!query) {
|
|
1965
|
+
throw new McpError3(ErrorCode3.InvalidRequest, 'Query parameter "q" is required');
|
|
1966
|
+
}
|
|
1967
|
+
const limit = parseInt(uri.searchParams.get("limit") || "10", 10);
|
|
1968
|
+
const minImportance = uri.searchParams.get("min_importance") ? parseInt(uri.searchParams.get("min_importance"), 10) : void 0;
|
|
1969
|
+
const results = await memoryStore3.searchMemories(query, limit, minImportance);
|
|
1970
|
+
return {
|
|
1971
|
+
contents: [
|
|
1972
|
+
{
|
|
1973
|
+
uri: uri.toString(),
|
|
1974
|
+
mimeType: "application/json",
|
|
1975
|
+
text: JSON.stringify(
|
|
1976
|
+
{
|
|
1977
|
+
query,
|
|
1978
|
+
count: results.length,
|
|
1979
|
+
results: results.map((r) => ({
|
|
1980
|
+
memory_id: r.id,
|
|
1981
|
+
content: r.content,
|
|
1982
|
+
summary: r.summary,
|
|
1983
|
+
context_type: r.context_type,
|
|
1984
|
+
importance: r.importance,
|
|
1985
|
+
tags: r.tags,
|
|
1986
|
+
similarity: r.similarity,
|
|
1987
|
+
timestamp: r.timestamp
|
|
1988
|
+
}))
|
|
1989
|
+
},
|
|
1990
|
+
null,
|
|
1991
|
+
2
|
|
1992
|
+
)
|
|
1993
|
+
}
|
|
1994
|
+
]
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
},
|
|
1998
|
+
"memory://analytics": {
|
|
1999
|
+
name: "Memory Analytics",
|
|
2000
|
+
description: "Get detailed analytics about memory usage and trends",
|
|
2001
|
+
mimeType: "text/markdown",
|
|
2002
|
+
handler: async (uri) => {
|
|
2003
|
+
const analytics = await getAnalytics();
|
|
2004
|
+
return {
|
|
2005
|
+
contents: [
|
|
2006
|
+
{
|
|
2007
|
+
uri: uri.toString(),
|
|
2008
|
+
mimeType: "text/markdown",
|
|
2009
|
+
text: analytics
|
|
2010
|
+
}
|
|
2011
|
+
]
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
// src/prompts/formatters.ts
|
|
2018
|
+
function formatWorkspaceContext(workspacePath, directives, decisions, patterns) {
|
|
2019
|
+
const sections = [];
|
|
2020
|
+
sections.push(`# Workspace Context: ${workspacePath}`);
|
|
2021
|
+
sections.push("");
|
|
2022
|
+
sections.push("*Critical information to remember for this project*");
|
|
2023
|
+
sections.push("");
|
|
2024
|
+
if (directives.length > 0) {
|
|
2025
|
+
sections.push("## \u{1F3AF} Critical Directives");
|
|
2026
|
+
sections.push("");
|
|
2027
|
+
const sorted = directives.sort((a, b) => b.importance - a.importance);
|
|
2028
|
+
for (const dir of sorted.slice(0, 10)) {
|
|
2029
|
+
sections.push(`- **[Importance: ${dir.importance}/10]** ${dir.content}`);
|
|
2030
|
+
if (dir.tags.length > 0) {
|
|
2031
|
+
sections.push(` *Tags: ${dir.tags.join(", ")}*`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
sections.push("");
|
|
2035
|
+
}
|
|
2036
|
+
if (decisions.length > 0) {
|
|
2037
|
+
sections.push("## \u{1F4A1} Key Decisions");
|
|
2038
|
+
sections.push("");
|
|
2039
|
+
const sorted = decisions.sort((a, b) => b.importance - a.importance);
|
|
2040
|
+
for (const dec of sorted.slice(0, 8)) {
|
|
2041
|
+
const age = getAgeString(dec.timestamp);
|
|
2042
|
+
sections.push(`- **[${age}]** ${dec.content}`);
|
|
2043
|
+
}
|
|
2044
|
+
sections.push("");
|
|
2045
|
+
}
|
|
2046
|
+
if (patterns.length > 0) {
|
|
2047
|
+
sections.push("## \u{1F527} Code Patterns & Conventions");
|
|
2048
|
+
sections.push("");
|
|
2049
|
+
const sorted = patterns.sort((a, b) => b.importance - a.importance);
|
|
2050
|
+
for (const pat of sorted.slice(0, 8)) {
|
|
2051
|
+
sections.push(`- ${pat.content}`);
|
|
2052
|
+
if (pat.tags.length > 0) {
|
|
2053
|
+
sections.push(` *Applies to: ${pat.tags.join(", ")}*`);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
sections.push("");
|
|
2057
|
+
}
|
|
2058
|
+
if (directives.length === 0 && decisions.length === 0 && patterns.length === 0) {
|
|
2059
|
+
sections.push("*No critical context stored yet. As we work, I'll remember important patterns and decisions.*");
|
|
2060
|
+
}
|
|
2061
|
+
sections.push("");
|
|
2062
|
+
sections.push("---");
|
|
2063
|
+
sections.push("*This context is automatically injected to help me remember important project conventions and decisions.*");
|
|
2064
|
+
return sections.join("\n");
|
|
2065
|
+
}
|
|
2066
|
+
function getAgeString(timestamp) {
|
|
2067
|
+
const ageMs = Date.now() - timestamp;
|
|
2068
|
+
const ageMinutes = Math.floor(ageMs / (1e3 * 60));
|
|
2069
|
+
const ageHours = Math.floor(ageMs / (1e3 * 60 * 60));
|
|
2070
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
2071
|
+
if (ageDays > 0) {
|
|
2072
|
+
return `${ageDays}d ago`;
|
|
2073
|
+
} else if (ageHours > 0) {
|
|
2074
|
+
return `${ageHours}h ago`;
|
|
2075
|
+
} else if (ageMinutes > 0) {
|
|
2076
|
+
return `${ageMinutes}m ago`;
|
|
2077
|
+
} else {
|
|
2078
|
+
return "just now";
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// src/prompts/index.ts
|
|
2083
|
+
var memoryStore4 = new MemoryStore();
|
|
2084
|
+
var prompts = {
|
|
2085
|
+
workspace_context: {
|
|
2086
|
+
name: "workspace_context",
|
|
2087
|
+
description: "Critical workspace context: directives, decisions, and code patterns",
|
|
2088
|
+
arguments: [],
|
|
2089
|
+
handler: async () => {
|
|
2090
|
+
const directives = await memoryStore4.getMemoriesByType("directive");
|
|
2091
|
+
const decisions = await memoryStore4.getMemoriesByType("decision");
|
|
2092
|
+
const patterns = await memoryStore4.getMemoriesByType("code_pattern");
|
|
2093
|
+
const importantDirectives = directives.filter((d) => d.importance >= 8);
|
|
2094
|
+
const importantDecisions = decisions.filter((d) => d.importance >= 7);
|
|
2095
|
+
const importantPatterns = patterns.filter((p) => p.importance >= 7);
|
|
2096
|
+
const stats = await memoryStore4.getSummaryStats();
|
|
2097
|
+
const workspacePath = stats.workspace_path;
|
|
2098
|
+
const contextText = formatWorkspaceContext(
|
|
2099
|
+
workspacePath,
|
|
2100
|
+
importantDirectives,
|
|
2101
|
+
importantDecisions,
|
|
2102
|
+
importantPatterns
|
|
2103
|
+
);
|
|
2104
|
+
return {
|
|
2105
|
+
description: "Workspace-specific context and conventions",
|
|
2106
|
+
messages: [
|
|
2107
|
+
{
|
|
2108
|
+
role: "user",
|
|
2109
|
+
content: {
|
|
2110
|
+
type: "text",
|
|
2111
|
+
text: contextText
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
]
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
async function listPrompts() {
|
|
2120
|
+
return Object.values(prompts).map((p) => ({
|
|
2121
|
+
name: p.name,
|
|
2122
|
+
description: p.description,
|
|
2123
|
+
arguments: p.arguments
|
|
2124
|
+
}));
|
|
2125
|
+
}
|
|
2126
|
+
async function getPrompt(name) {
|
|
2127
|
+
const prompt = prompts[name];
|
|
2128
|
+
if (!prompt) {
|
|
2129
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
2130
|
+
}
|
|
2131
|
+
return await prompt.handler();
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// src/index.ts
|
|
2135
|
+
var server = new Server(
|
|
2136
|
+
{
|
|
2137
|
+
name: "@joseairosa/recall",
|
|
2138
|
+
version: "1.2.0"
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
capabilities: {
|
|
2142
|
+
resources: {},
|
|
2143
|
+
tools: {},
|
|
2144
|
+
prompts: {}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
);
|
|
2148
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2149
|
+
return {
|
|
2150
|
+
tools: Object.entries(tools).map(([name, tool]) => ({
|
|
2151
|
+
name,
|
|
2152
|
+
description: tool.description,
|
|
2153
|
+
inputSchema: tool.inputSchema
|
|
2154
|
+
}))
|
|
2155
|
+
};
|
|
2156
|
+
});
|
|
2157
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2158
|
+
const { name, arguments: args } = request.params;
|
|
2159
|
+
const tool = tools[name];
|
|
2160
|
+
if (!tool) {
|
|
2161
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2162
|
+
}
|
|
2163
|
+
return await tool.handler(args);
|
|
2164
|
+
});
|
|
2165
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
2166
|
+
return {
|
|
2167
|
+
resources: [
|
|
2168
|
+
{
|
|
2169
|
+
uri: "memory://recent",
|
|
2170
|
+
name: "Recent Memories",
|
|
2171
|
+
description: "Get the most recent memories (default: 50)",
|
|
2172
|
+
mimeType: "application/json"
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
uri: "memory://by-type/{type}",
|
|
2176
|
+
name: "Memories by Type",
|
|
2177
|
+
description: "Get memories filtered by context type (directive, information, heading, decision, code_pattern, requirement, error, todo, insight, preference)",
|
|
2178
|
+
mimeType: "application/json"
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
uri: "memory://by-tag/{tag}",
|
|
2182
|
+
name: "Memories by Tag",
|
|
2183
|
+
description: "Get memories filtered by tag",
|
|
2184
|
+
mimeType: "application/json"
|
|
2185
|
+
},
|
|
2186
|
+
{
|
|
2187
|
+
uri: "memory://important",
|
|
2188
|
+
name: "Important Memories",
|
|
2189
|
+
description: "Get high-importance memories (importance >= 8)",
|
|
2190
|
+
mimeType: "application/json"
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
uri: "memory://session/{session_id}",
|
|
2194
|
+
name: "Session Memories",
|
|
2195
|
+
description: "Get all memories in a specific session",
|
|
2196
|
+
mimeType: "application/json"
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
uri: "memory://sessions",
|
|
2200
|
+
name: "All Sessions",
|
|
2201
|
+
description: "Get list of all sessions",
|
|
2202
|
+
mimeType: "application/json"
|
|
2203
|
+
},
|
|
2204
|
+
{
|
|
2205
|
+
uri: "memory://summary",
|
|
2206
|
+
name: "Memory Summary",
|
|
2207
|
+
description: "Get overall summary statistics",
|
|
2208
|
+
mimeType: "application/json"
|
|
2209
|
+
},
|
|
2210
|
+
{
|
|
2211
|
+
uri: "memory://search",
|
|
2212
|
+
name: "Search Memories",
|
|
2213
|
+
description: 'Search memories using semantic similarity. Requires query parameter "q"',
|
|
2214
|
+
mimeType: "application/json"
|
|
2215
|
+
}
|
|
2216
|
+
]
|
|
2217
|
+
};
|
|
2218
|
+
});
|
|
2219
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2220
|
+
const uriString = request.params.uri;
|
|
2221
|
+
const uri = new URL(uriString);
|
|
2222
|
+
const resourcePath = uri.hostname + uri.pathname;
|
|
2223
|
+
if (resourcePath === "recent") {
|
|
2224
|
+
return await resources["memory://recent"].handler(uri);
|
|
2225
|
+
}
|
|
2226
|
+
if (resourcePath === "important") {
|
|
2227
|
+
return await resources["memory://important"].handler(uri);
|
|
2228
|
+
}
|
|
2229
|
+
if (resourcePath === "sessions") {
|
|
2230
|
+
return await resources["memory://sessions"].handler(uri);
|
|
2231
|
+
}
|
|
2232
|
+
if (resourcePath === "summary") {
|
|
2233
|
+
return await resources["memory://summary"].handler(uri);
|
|
2234
|
+
}
|
|
2235
|
+
if (resourcePath === "search") {
|
|
2236
|
+
return await resources["memory://search"].handler(uri);
|
|
2237
|
+
}
|
|
2238
|
+
if (resourcePath === "analytics") {
|
|
2239
|
+
return await resources["memory://analytics"].handler(uri);
|
|
2240
|
+
}
|
|
2241
|
+
const typeMatch = resourcePath.match(/^by-type\/(.+)$/);
|
|
2242
|
+
if (typeMatch) {
|
|
2243
|
+
return await resources["memory://by-type/{type}"].handler(uri, { type: typeMatch[1] });
|
|
2244
|
+
}
|
|
2245
|
+
const tagMatch = resourcePath.match(/^by-tag\/(.+)$/);
|
|
2246
|
+
if (tagMatch) {
|
|
2247
|
+
return await resources["memory://by-tag/{tag}"].handler(uri, { tag: tagMatch[1] });
|
|
2248
|
+
}
|
|
2249
|
+
const sessionMatch = resourcePath.match(/^session\/(.+)$/);
|
|
2250
|
+
if (sessionMatch) {
|
|
2251
|
+
return await resources["memory://session/{session_id}"].handler(uri, { session_id: sessionMatch[1] });
|
|
2252
|
+
}
|
|
2253
|
+
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
2254
|
+
});
|
|
2255
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
2256
|
+
const promptsList = await listPrompts();
|
|
2257
|
+
return {
|
|
2258
|
+
prompts: promptsList
|
|
2259
|
+
};
|
|
2260
|
+
});
|
|
2261
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
2262
|
+
const promptResult = await getPrompt(request.params.name);
|
|
2263
|
+
return promptResult;
|
|
2264
|
+
});
|
|
2265
|
+
async function main() {
|
|
2266
|
+
console.error("Checking Redis connection...");
|
|
2267
|
+
const isConnected = await checkRedisConnection();
|
|
2268
|
+
if (!isConnected) {
|
|
2269
|
+
console.error("ERROR: Failed to connect to Redis");
|
|
2270
|
+
console.error("Please ensure Redis is running and REDIS_URL is set correctly");
|
|
2271
|
+
process.exit(1);
|
|
2272
|
+
}
|
|
2273
|
+
console.error("Redis connection successful");
|
|
2274
|
+
const transport = new StdioServerTransport();
|
|
2275
|
+
await server.connect(transport);
|
|
2276
|
+
console.error("Recall MCP Server started successfully");
|
|
2277
|
+
console.error("Listening on stdio...");
|
|
2278
|
+
}
|
|
2279
|
+
process.on("SIGINT", async () => {
|
|
2280
|
+
console.error("\nShutting down...");
|
|
2281
|
+
await closeRedisClient();
|
|
2282
|
+
process.exit(0);
|
|
2283
|
+
});
|
|
2284
|
+
process.on("SIGTERM", async () => {
|
|
2285
|
+
console.error("\nShutting down...");
|
|
2286
|
+
await closeRedisClient();
|
|
2287
|
+
process.exit(0);
|
|
2288
|
+
});
|
|
2289
|
+
main().catch((error) => {
|
|
2290
|
+
console.error("Fatal error:", error);
|
|
2291
|
+
process.exit(1);
|
|
2292
|
+
});
|
|
2293
|
+
//# sourceMappingURL=index.js.map
|