@asd412id/mcp-context-manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +35 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +127 -0
- package/dist/storage/file-store.d.ts +18 -0
- package/dist/storage/file-store.js +84 -0
- package/dist/tools/checkpoint.d.ts +2 -0
- package/dist/tools/checkpoint.js +192 -0
- package/dist/tools/loader.d.ts +2 -0
- package/dist/tools/loader.js +263 -0
- package/dist/tools/memory.d.ts +2 -0
- package/dist/tools/memory.js +182 -0
- package/dist/tools/summarizer.d.ts +2 -0
- package/dist/tools/summarizer.js +228 -0
- package/dist/tools/tracker.d.ts +2 -0
- package/dist/tools/tracker.js +196 -0
- package/package.json +47 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
function readFileLines(filePath, startLine, endLine) {
|
|
5
|
+
try {
|
|
6
|
+
if (!fs.existsSync(filePath))
|
|
7
|
+
return null;
|
|
8
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
9
|
+
const lines = content.split('\n');
|
|
10
|
+
const start = Math.max(0, startLine - 1);
|
|
11
|
+
const end = Math.min(lines.length, endLine);
|
|
12
|
+
return {
|
|
13
|
+
startLine: start + 1,
|
|
14
|
+
endLine: end,
|
|
15
|
+
content: lines.slice(start, end).join('\n')
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function getFileInfo(filePath) {
|
|
23
|
+
const info = {
|
|
24
|
+
path: filePath,
|
|
25
|
+
exists: fs.existsSync(filePath)
|
|
26
|
+
};
|
|
27
|
+
if (info.exists) {
|
|
28
|
+
try {
|
|
29
|
+
const stats = fs.statSync(filePath);
|
|
30
|
+
info.size = stats.size;
|
|
31
|
+
info.modifiedAt = stats.mtime.toISOString();
|
|
32
|
+
info.extension = path.extname(filePath);
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
info.lines = content.split('\n').length;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// ignore errors
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return info;
|
|
41
|
+
}
|
|
42
|
+
function findRelevantSections(content, keywords) {
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
const results = [];
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
const line = lines[i].toLowerCase();
|
|
47
|
+
for (const keyword of keywords) {
|
|
48
|
+
if (line.includes(keyword.toLowerCase())) {
|
|
49
|
+
results.push({ line: i + 1, text: lines[i] });
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
function extractCodeStructure(content, extension) {
|
|
57
|
+
const structures = [];
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
const patterns = {
|
|
60
|
+
'.ts': [
|
|
61
|
+
/^export\s+(async\s+)?function\s+(\w+)/,
|
|
62
|
+
/^export\s+(const|let|var)\s+(\w+)/,
|
|
63
|
+
/^export\s+(class|interface|type|enum)\s+(\w+)/,
|
|
64
|
+
/^(class|interface|type|enum)\s+(\w+)/,
|
|
65
|
+
/^(async\s+)?function\s+(\w+)/
|
|
66
|
+
],
|
|
67
|
+
'.js': [
|
|
68
|
+
/^export\s+(async\s+)?function\s+(\w+)/,
|
|
69
|
+
/^export\s+(const|let|var)\s+(\w+)/,
|
|
70
|
+
/^export\s+class\s+(\w+)/,
|
|
71
|
+
/^class\s+(\w+)/,
|
|
72
|
+
/^(async\s+)?function\s+(\w+)/,
|
|
73
|
+
/^(const|let|var)\s+(\w+)\s*=/
|
|
74
|
+
],
|
|
75
|
+
'.py': [
|
|
76
|
+
/^def\s+(\w+)/,
|
|
77
|
+
/^class\s+(\w+)/,
|
|
78
|
+
/^async\s+def\s+(\w+)/
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
const applicablePatterns = patterns[extension] || patterns['.js'];
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i].trim();
|
|
84
|
+
for (const pattern of applicablePatterns) {
|
|
85
|
+
const match = line.match(pattern);
|
|
86
|
+
if (match) {
|
|
87
|
+
structures.push(`L${i + 1}: ${line.substring(0, 80)}${line.length > 80 ? '...' : ''}`);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return structures;
|
|
93
|
+
}
|
|
94
|
+
export function registerLoaderTools(server) {
|
|
95
|
+
server.registerTool('file_smart_read', {
|
|
96
|
+
title: 'Smart File Read',
|
|
97
|
+
description: 'Read a file with smart options: specific lines, keyword search, or structure extraction. Optimized for context efficiency.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
path: z.string().describe('File path to read'),
|
|
100
|
+
startLine: z.number().optional().describe('Start line (1-indexed)'),
|
|
101
|
+
endLine: z.number().optional().describe('End line'),
|
|
102
|
+
keywords: z.array(z.string()).optional().describe('Only return lines containing these keywords'),
|
|
103
|
+
structureOnly: z.boolean().optional().describe('Return only code structure (functions, classes, etc.)'),
|
|
104
|
+
maxLines: z.number().optional().describe('Maximum lines to return (default: 500)')
|
|
105
|
+
}
|
|
106
|
+
}, async ({ path: filePath, startLine, endLine, keywords, structureOnly, maxLines = 500 }) => {
|
|
107
|
+
const info = getFileInfo(filePath);
|
|
108
|
+
if (!info.exists) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: 'text', text: `File not found: ${filePath}` }]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
114
|
+
if (structureOnly && info.extension) {
|
|
115
|
+
const structure = extractCodeStructure(content, info.extension);
|
|
116
|
+
return {
|
|
117
|
+
content: [{
|
|
118
|
+
type: 'text',
|
|
119
|
+
text: JSON.stringify({
|
|
120
|
+
file: filePath,
|
|
121
|
+
totalLines: info.lines,
|
|
122
|
+
structure: structure
|
|
123
|
+
}, null, 2)
|
|
124
|
+
}]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (keywords && keywords.length > 0) {
|
|
128
|
+
const sections = findRelevantSections(content, keywords);
|
|
129
|
+
const limited = sections.slice(0, maxLines);
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: JSON.stringify({
|
|
134
|
+
file: filePath,
|
|
135
|
+
totalLines: info.lines,
|
|
136
|
+
matchedLines: sections.length,
|
|
137
|
+
matches: limited
|
|
138
|
+
}, null, 2)
|
|
139
|
+
}]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (startLine || endLine) {
|
|
143
|
+
const start = startLine || 1;
|
|
144
|
+
const end = endLine || (info.lines || 1000);
|
|
145
|
+
const section = readFileLines(filePath, start, Math.min(end, start + maxLines - 1));
|
|
146
|
+
if (section) {
|
|
147
|
+
return {
|
|
148
|
+
content: [{
|
|
149
|
+
type: 'text',
|
|
150
|
+
text: `File: ${filePath} (lines ${section.startLine}-${section.endLine} of ${info.lines})\n\n${section.content}`
|
|
151
|
+
}]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const lines = content.split('\n');
|
|
156
|
+
const limitedContent = lines.slice(0, maxLines).join('\n');
|
|
157
|
+
const truncated = lines.length > maxLines;
|
|
158
|
+
return {
|
|
159
|
+
content: [{
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: `File: ${filePath} (${info.lines} lines${truncated ? `, showing first ${maxLines}` : ''})\n\n${limitedContent}${truncated ? '\n\n... [truncated]' : ''}`
|
|
162
|
+
}]
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
server.registerTool('file_info', {
|
|
166
|
+
title: 'File Info',
|
|
167
|
+
description: 'Get file metadata without reading content. Use to check file existence, size, and modification time.',
|
|
168
|
+
inputSchema: {
|
|
169
|
+
paths: z.array(z.string()).describe('File paths to check')
|
|
170
|
+
}
|
|
171
|
+
}, async ({ paths }) => {
|
|
172
|
+
const infos = paths.map(p => getFileInfo(p));
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: 'text', text: JSON.stringify(infos, null, 2) }]
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
server.registerTool('file_search_content', {
|
|
178
|
+
title: 'Search File Content',
|
|
179
|
+
description: 'Search for patterns in a file and return matching lines with context.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
path: z.string().describe('File path to search'),
|
|
182
|
+
pattern: z.string().describe('Search pattern (supports regex)'),
|
|
183
|
+
contextLines: z.number().optional().describe('Number of context lines before/after match (default: 2)')
|
|
184
|
+
}
|
|
185
|
+
}, async ({ path: filePath, pattern, contextLines = 2 }) => {
|
|
186
|
+
if (!fs.existsSync(filePath)) {
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: 'text', text: `File not found: ${filePath}` }]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
192
|
+
const lines = content.split('\n');
|
|
193
|
+
const regex = new RegExp(pattern, 'gi');
|
|
194
|
+
const matches = [];
|
|
195
|
+
for (let i = 0; i < lines.length; i++) {
|
|
196
|
+
if (regex.test(lines[i])) {
|
|
197
|
+
const start = Math.max(0, i - contextLines);
|
|
198
|
+
const end = Math.min(lines.length, i + contextLines + 1);
|
|
199
|
+
const context = lines.slice(start, end).map((l, idx) => `${start + idx + 1}${start + idx === i ? '>' : ':'} ${l}`);
|
|
200
|
+
matches.push({
|
|
201
|
+
line: i + 1,
|
|
202
|
+
match: lines[i],
|
|
203
|
+
context
|
|
204
|
+
});
|
|
205
|
+
regex.lastIndex = 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
content: [{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: matches.length > 0
|
|
212
|
+
? JSON.stringify({ file: filePath, matches }, null, 2)
|
|
213
|
+
: `No matches found for pattern "${pattern}" in ${filePath}`
|
|
214
|
+
}]
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
server.registerTool('file_list_dir', {
|
|
218
|
+
title: 'List Directory',
|
|
219
|
+
description: 'List files in a directory with optional filtering.',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
path: z.string().describe('Directory path'),
|
|
222
|
+
pattern: z.string().optional().describe('File name pattern (e.g., "*.ts")'),
|
|
223
|
+
recursive: z.boolean().optional().describe('Include subdirectories (default: false)')
|
|
224
|
+
}
|
|
225
|
+
}, async ({ path: dirPath, pattern, recursive = false }) => {
|
|
226
|
+
if (!fs.existsSync(dirPath)) {
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: 'text', text: `Directory not found: ${dirPath}` }]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const results = [];
|
|
232
|
+
function walkDir(dir) {
|
|
233
|
+
const items = fs.readdirSync(dir);
|
|
234
|
+
for (const item of items) {
|
|
235
|
+
const fullPath = path.join(dir, item);
|
|
236
|
+
const stat = fs.statSync(fullPath);
|
|
237
|
+
if (stat.isDirectory() && recursive) {
|
|
238
|
+
walkDir(fullPath);
|
|
239
|
+
}
|
|
240
|
+
else if (stat.isFile()) {
|
|
241
|
+
if (pattern) {
|
|
242
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
243
|
+
if (regex.test(item)) {
|
|
244
|
+
results.push(fullPath);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
results.push(fullPath);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
walkDir(dirPath);
|
|
254
|
+
return {
|
|
255
|
+
content: [{
|
|
256
|
+
type: 'text',
|
|
257
|
+
text: results.length > 0
|
|
258
|
+
? results.join('\n')
|
|
259
|
+
: 'No files found'
|
|
260
|
+
}]
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { getStore } from '../storage/file-store.js';
|
|
3
|
+
const MEMORY_FILE = 'memory.json';
|
|
4
|
+
async function getMemoryStore() {
|
|
5
|
+
const store = getStore();
|
|
6
|
+
return store.read(MEMORY_FILE, { entries: {} });
|
|
7
|
+
}
|
|
8
|
+
async function saveMemoryStore(data) {
|
|
9
|
+
const store = getStore();
|
|
10
|
+
await store.write(MEMORY_FILE, data);
|
|
11
|
+
}
|
|
12
|
+
function isExpired(entry) {
|
|
13
|
+
if (!entry.ttl)
|
|
14
|
+
return false;
|
|
15
|
+
const expiresAt = new Date(entry.createdAt).getTime() + entry.ttl;
|
|
16
|
+
return Date.now() > expiresAt;
|
|
17
|
+
}
|
|
18
|
+
export function registerMemoryTools(server) {
|
|
19
|
+
server.registerTool('memory_set', {
|
|
20
|
+
title: 'Memory Set',
|
|
21
|
+
description: 'Store a key-value pair in persistent memory. Use for saving important context, decisions, or data across sessions.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
key: z.string().describe('Unique identifier for this memory'),
|
|
24
|
+
value: z.unknown().describe('Data to store (any JSON-serializable value)'),
|
|
25
|
+
tags: z.array(z.string()).optional().describe('Tags for categorization and searching'),
|
|
26
|
+
ttl: z.number().optional().describe('Time-to-live in milliseconds (optional)')
|
|
27
|
+
}
|
|
28
|
+
}, async ({ key, value, tags, ttl }) => {
|
|
29
|
+
const memStore = await getMemoryStore();
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
const existing = memStore.entries[key];
|
|
32
|
+
memStore.entries[key] = {
|
|
33
|
+
key,
|
|
34
|
+
value,
|
|
35
|
+
tags: tags || [],
|
|
36
|
+
createdAt: existing?.createdAt || now,
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
ttl
|
|
39
|
+
};
|
|
40
|
+
await saveMemoryStore(memStore);
|
|
41
|
+
return {
|
|
42
|
+
content: [{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: `Memory saved: "${key}"${ttl ? ` (expires in ${ttl}ms)` : ''}`
|
|
45
|
+
}]
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
server.registerTool('memory_get', {
|
|
49
|
+
title: 'Memory Get',
|
|
50
|
+
description: 'Retrieve a value from persistent memory by key.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
key: z.string().describe('Key to retrieve')
|
|
53
|
+
}
|
|
54
|
+
}, async ({ key }) => {
|
|
55
|
+
const memStore = await getMemoryStore();
|
|
56
|
+
const entry = memStore.entries[key];
|
|
57
|
+
if (!entry) {
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: `Memory not found: "${key}"` }]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (isExpired(entry)) {
|
|
63
|
+
delete memStore.entries[key];
|
|
64
|
+
await saveMemoryStore(memStore);
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: 'text', text: `Memory expired: "${key}"` }]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
content: [{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: JSON.stringify({
|
|
73
|
+
key: entry.key,
|
|
74
|
+
value: entry.value,
|
|
75
|
+
tags: entry.tags,
|
|
76
|
+
createdAt: entry.createdAt,
|
|
77
|
+
updatedAt: entry.updatedAt
|
|
78
|
+
}, null, 2)
|
|
79
|
+
}]
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
server.registerTool('memory_search', {
|
|
83
|
+
title: 'Memory Search',
|
|
84
|
+
description: 'Search memories by key pattern or tags.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
pattern: z.string().optional().describe('Key pattern to search (supports * wildcard)'),
|
|
87
|
+
tags: z.array(z.string()).optional().describe('Filter by tags (any match)')
|
|
88
|
+
}
|
|
89
|
+
}, async ({ pattern, tags }) => {
|
|
90
|
+
const memStore = await getMemoryStore();
|
|
91
|
+
let results = Object.values(memStore.entries);
|
|
92
|
+
results = results.filter(entry => !isExpired(entry));
|
|
93
|
+
if (pattern) {
|
|
94
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
|
|
95
|
+
results = results.filter(entry => regex.test(entry.key));
|
|
96
|
+
}
|
|
97
|
+
if (tags && tags.length > 0) {
|
|
98
|
+
results = results.filter(entry => tags.some(tag => entry.tags.includes(tag)));
|
|
99
|
+
}
|
|
100
|
+
const output = results.map(entry => ({
|
|
101
|
+
key: entry.key,
|
|
102
|
+
value: entry.value,
|
|
103
|
+
tags: entry.tags,
|
|
104
|
+
updatedAt: entry.updatedAt
|
|
105
|
+
}));
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: results.length > 0
|
|
110
|
+
? JSON.stringify(output, null, 2)
|
|
111
|
+
: 'No memories found matching criteria'
|
|
112
|
+
}]
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
server.registerTool('memory_delete', {
|
|
116
|
+
title: 'Memory Delete',
|
|
117
|
+
description: 'Delete a memory entry by key.',
|
|
118
|
+
inputSchema: {
|
|
119
|
+
key: z.string().describe('Key to delete')
|
|
120
|
+
}
|
|
121
|
+
}, async ({ key }) => {
|
|
122
|
+
const memStore = await getMemoryStore();
|
|
123
|
+
if (!memStore.entries[key]) {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: 'text', text: `Memory not found: "${key}"` }]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
delete memStore.entries[key];
|
|
129
|
+
await saveMemoryStore(memStore);
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: 'text', text: `Memory deleted: "${key}"` }]
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
server.registerTool('memory_list', {
|
|
135
|
+
title: 'Memory List',
|
|
136
|
+
description: 'List all memory keys with their tags.',
|
|
137
|
+
inputSchema: {}
|
|
138
|
+
}, async () => {
|
|
139
|
+
const memStore = await getMemoryStore();
|
|
140
|
+
const entries = Object.values(memStore.entries).filter(e => !isExpired(e));
|
|
141
|
+
if (entries.length === 0) {
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: 'text', text: 'No memories stored' }]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const list = entries.map(e => ({
|
|
147
|
+
key: e.key,
|
|
148
|
+
tags: e.tags,
|
|
149
|
+
updatedAt: e.updatedAt
|
|
150
|
+
}));
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: 'text', text: JSON.stringify(list, null, 2) }]
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
server.registerTool('memory_clear', {
|
|
156
|
+
title: 'Memory Clear',
|
|
157
|
+
description: 'Clear all memories or memories matching specific tags.',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
tags: z.array(z.string()).optional().describe('Only clear memories with these tags (clears all if not specified)')
|
|
160
|
+
}
|
|
161
|
+
}, async ({ tags }) => {
|
|
162
|
+
const memStore = await getMemoryStore();
|
|
163
|
+
let count = 0;
|
|
164
|
+
if (tags && tags.length > 0) {
|
|
165
|
+
for (const key of Object.keys(memStore.entries)) {
|
|
166
|
+
const entry = memStore.entries[key];
|
|
167
|
+
if (tags.some(tag => entry.tags.includes(tag))) {
|
|
168
|
+
delete memStore.entries[key];
|
|
169
|
+
count++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
count = Object.keys(memStore.entries).length;
|
|
175
|
+
memStore.entries = {};
|
|
176
|
+
}
|
|
177
|
+
await saveMemoryStore(memStore);
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: 'text', text: `Cleared ${count} memories` }]
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { getStore } from '../storage/file-store.js';
|
|
3
|
+
const SUMMARIES_DIR = 'summaries';
|
|
4
|
+
async function getSummaryStore() {
|
|
5
|
+
const store = getStore().getSubStore(SUMMARIES_DIR);
|
|
6
|
+
return store.read('index.json', { summaries: [] });
|
|
7
|
+
}
|
|
8
|
+
async function saveSummaryStore(data) {
|
|
9
|
+
const store = getStore().getSubStore(SUMMARIES_DIR);
|
|
10
|
+
await store.write('index.json', data);
|
|
11
|
+
}
|
|
12
|
+
function extractKeyPoints(text) {
|
|
13
|
+
const points = [];
|
|
14
|
+
const lines = text.split('\n');
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (trimmed.match(/^[-*•]\s+/) || trimmed.match(/^\d+\.\s+/)) {
|
|
18
|
+
points.push(trimmed.replace(/^[-*•\d.]+\s*/, ''));
|
|
19
|
+
}
|
|
20
|
+
if (trimmed.toLowerCase().includes('important:') ||
|
|
21
|
+
trimmed.toLowerCase().includes('note:') ||
|
|
22
|
+
trimmed.toLowerCase().includes('key:')) {
|
|
23
|
+
points.push(trimmed);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return points.slice(0, 10);
|
|
27
|
+
}
|
|
28
|
+
function extractDecisions(text) {
|
|
29
|
+
const decisions = [];
|
|
30
|
+
const patterns = [
|
|
31
|
+
/decided to ([^.]+)/gi,
|
|
32
|
+
/will use ([^.]+)/gi,
|
|
33
|
+
/chose ([^.]+)/gi,
|
|
34
|
+
/agreed on ([^.]+)/gi,
|
|
35
|
+
/decision:\s*([^.\n]+)/gi
|
|
36
|
+
];
|
|
37
|
+
for (const pattern of patterns) {
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
40
|
+
decisions.push(match[1].trim());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...new Set(decisions)].slice(0, 5);
|
|
44
|
+
}
|
|
45
|
+
function extractActionItems(text) {
|
|
46
|
+
const actions = [];
|
|
47
|
+
const patterns = [
|
|
48
|
+
/todo:\s*([^.\n]+)/gi,
|
|
49
|
+
/action:\s*([^.\n]+)/gi,
|
|
50
|
+
/need to ([^.]+)/gi,
|
|
51
|
+
/should ([^.]+)/gi,
|
|
52
|
+
/must ([^.]+)/gi,
|
|
53
|
+
/\[ \]\s*([^\n]+)/g
|
|
54
|
+
];
|
|
55
|
+
for (const pattern of patterns) {
|
|
56
|
+
let match;
|
|
57
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
58
|
+
actions.push(match[1].trim());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...new Set(actions)].slice(0, 10);
|
|
62
|
+
}
|
|
63
|
+
function compressText(text, maxLength) {
|
|
64
|
+
if (text.length <= maxLength)
|
|
65
|
+
return text;
|
|
66
|
+
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
|
|
67
|
+
const important = [];
|
|
68
|
+
const normal = [];
|
|
69
|
+
for (const sentence of sentences) {
|
|
70
|
+
const lower = sentence.toLowerCase();
|
|
71
|
+
if (lower.includes('important') ||
|
|
72
|
+
lower.includes('key') ||
|
|
73
|
+
lower.includes('must') ||
|
|
74
|
+
lower.includes('error') ||
|
|
75
|
+
lower.includes('decision')) {
|
|
76
|
+
important.push(sentence.trim());
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
normal.push(sentence.trim());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let result = important.join(' ');
|
|
83
|
+
for (const sentence of normal) {
|
|
84
|
+
if ((result + ' ' + sentence).length <= maxLength) {
|
|
85
|
+
result += ' ' + sentence;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result.trim() || text.slice(0, maxLength) + '...';
|
|
89
|
+
}
|
|
90
|
+
export function registerSummarizerTools(server) {
|
|
91
|
+
server.registerTool('context_summarize', {
|
|
92
|
+
title: 'Context Summarize',
|
|
93
|
+
description: 'Summarize and compress context/conversation. Extracts key points, decisions, and action items. Use this when context is getting too long.',
|
|
94
|
+
inputSchema: {
|
|
95
|
+
text: z.string().describe('Text to summarize'),
|
|
96
|
+
maxLength: z.number().optional().describe('Maximum length for compressed summary (default: 2000)'),
|
|
97
|
+
sessionId: z.string().optional().describe('Session identifier for grouping summaries')
|
|
98
|
+
}
|
|
99
|
+
}, async ({ text, maxLength = 2000, sessionId }) => {
|
|
100
|
+
const keyPoints = extractKeyPoints(text);
|
|
101
|
+
const decisions = extractDecisions(text);
|
|
102
|
+
const actionItems = extractActionItems(text);
|
|
103
|
+
const compressed = compressText(text, maxLength);
|
|
104
|
+
const summary = {
|
|
105
|
+
id: `sum_${Date.now()}`,
|
|
106
|
+
originalLength: text.length,
|
|
107
|
+
summaryLength: compressed.length,
|
|
108
|
+
keyPoints,
|
|
109
|
+
decisions,
|
|
110
|
+
actionItems,
|
|
111
|
+
context: compressed,
|
|
112
|
+
createdAt: new Date().toISOString(),
|
|
113
|
+
sessionId
|
|
114
|
+
};
|
|
115
|
+
const store = await getSummaryStore();
|
|
116
|
+
store.summaries.push(summary);
|
|
117
|
+
await saveSummaryStore(store);
|
|
118
|
+
const output = {
|
|
119
|
+
id: summary.id,
|
|
120
|
+
compression: `${summary.originalLength} -> ${summary.summaryLength} chars (${Math.round((1 - summary.summaryLength / summary.originalLength) * 100)}% reduced)`,
|
|
121
|
+
keyPoints,
|
|
122
|
+
decisions,
|
|
123
|
+
actionItems,
|
|
124
|
+
summary: compressed
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
server.registerTool('context_get_summary', {
|
|
131
|
+
title: 'Get Summary',
|
|
132
|
+
description: 'Retrieve a previously saved summary by ID.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
id: z.string().describe('Summary ID to retrieve')
|
|
135
|
+
}
|
|
136
|
+
}, async ({ id }) => {
|
|
137
|
+
const store = await getSummaryStore();
|
|
138
|
+
const summary = store.summaries.find(s => s.id === id);
|
|
139
|
+
if (!summary) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: `Summary not found: ${id}` }]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }]
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
server.registerTool('context_list_summaries', {
|
|
149
|
+
title: 'List Summaries',
|
|
150
|
+
description: 'List all saved summaries with metadata.',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
sessionId: z.string().optional().describe('Filter by session ID'),
|
|
153
|
+
limit: z.number().optional().describe('Maximum number of summaries to return (default: 10)')
|
|
154
|
+
}
|
|
155
|
+
}, async ({ sessionId, limit = 10 }) => {
|
|
156
|
+
const store = await getSummaryStore();
|
|
157
|
+
let summaries = store.summaries;
|
|
158
|
+
if (sessionId) {
|
|
159
|
+
summaries = summaries.filter(s => s.sessionId === sessionId);
|
|
160
|
+
}
|
|
161
|
+
summaries = summaries.slice(-limit);
|
|
162
|
+
const list = summaries.map(s => ({
|
|
163
|
+
id: s.id,
|
|
164
|
+
createdAt: s.createdAt,
|
|
165
|
+
originalLength: s.originalLength,
|
|
166
|
+
summaryLength: s.summaryLength,
|
|
167
|
+
keyPointsCount: s.keyPoints.length,
|
|
168
|
+
decisionsCount: s.decisions.length,
|
|
169
|
+
actionItemsCount: s.actionItems.length,
|
|
170
|
+
sessionId: s.sessionId
|
|
171
|
+
}));
|
|
172
|
+
return {
|
|
173
|
+
content: [{
|
|
174
|
+
type: 'text',
|
|
175
|
+
text: list.length > 0
|
|
176
|
+
? JSON.stringify(list, null, 2)
|
|
177
|
+
: 'No summaries found'
|
|
178
|
+
}]
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
server.registerTool('context_merge_summaries', {
|
|
182
|
+
title: 'Merge Summaries',
|
|
183
|
+
description: 'Merge multiple summaries into a single consolidated summary. Useful for combining session history.',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
ids: z.array(z.string()).describe('Summary IDs to merge'),
|
|
186
|
+
maxLength: z.number().optional().describe('Max length for merged summary (default: 4000)')
|
|
187
|
+
}
|
|
188
|
+
}, async ({ ids, maxLength = 4000 }) => {
|
|
189
|
+
const store = await getSummaryStore();
|
|
190
|
+
const summaries = store.summaries.filter(s => ids.includes(s.id));
|
|
191
|
+
if (summaries.length === 0) {
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: 'text', text: 'No summaries found with provided IDs' }]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const allKeyPoints = [...new Set(summaries.flatMap(s => s.keyPoints))];
|
|
197
|
+
const allDecisions = [...new Set(summaries.flatMap(s => s.decisions))];
|
|
198
|
+
const allActionItems = [...new Set(summaries.flatMap(s => s.actionItems))];
|
|
199
|
+
const combinedContext = summaries.map(s => s.context).join('\n\n---\n\n');
|
|
200
|
+
const compressedContext = compressText(combinedContext, maxLength);
|
|
201
|
+
const merged = {
|
|
202
|
+
id: `merged_${Date.now()}`,
|
|
203
|
+
originalLength: summaries.reduce((acc, s) => acc + s.originalLength, 0),
|
|
204
|
+
summaryLength: compressedContext.length,
|
|
205
|
+
keyPoints: allKeyPoints.slice(0, 15),
|
|
206
|
+
decisions: allDecisions.slice(0, 10),
|
|
207
|
+
actionItems: allActionItems.slice(0, 15),
|
|
208
|
+
context: compressedContext,
|
|
209
|
+
createdAt: new Date().toISOString()
|
|
210
|
+
};
|
|
211
|
+
store.summaries.push(merged);
|
|
212
|
+
await saveSummaryStore(store);
|
|
213
|
+
return {
|
|
214
|
+
content: [{
|
|
215
|
+
type: 'text',
|
|
216
|
+
text: JSON.stringify({
|
|
217
|
+
id: merged.id,
|
|
218
|
+
mergedCount: summaries.length,
|
|
219
|
+
compression: `${merged.originalLength} -> ${merged.summaryLength} chars`,
|
|
220
|
+
keyPoints: merged.keyPoints,
|
|
221
|
+
decisions: merged.decisions,
|
|
222
|
+
actionItems: merged.actionItems,
|
|
223
|
+
summary: merged.context
|
|
224
|
+
}, null, 2)
|
|
225
|
+
}]
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
}
|