@cachly-dev/mcp-server 0.9.19 → 0.9.21
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/.github/workflows/publish.yml +42 -0
- package/LICENSE +202 -0
- package/dist/handlers/advanced.d.ts +6 -0
- package/dist/handlers/advanced.js +586 -0
- package/dist/handlers/advanced.js.map +1 -0
- package/dist/handlers/cache.d.ts +6 -0
- package/dist/handlers/cache.js +520 -0
- package/dist/handlers/cache.js.map +1 -0
- package/dist/handlers/context.d.ts +6 -0
- package/dist/handlers/context.js +177 -0
- package/dist/handlers/context.js.map +1 -0
- package/dist/handlers/fedbrain.d.ts +6 -0
- package/dist/handlers/fedbrain.js +1078 -0
- package/dist/handlers/fedbrain.js.map +1 -0
- package/dist/handlers/instances.d.ts +9 -0
- package/dist/handlers/instances.js +153 -0
- package/dist/handlers/instances.js.map +1 -0
- package/dist/handlers/roadmap.d.ts +6 -0
- package/dist/handlers/roadmap.js +165 -0
- package/dist/handlers/roadmap.js.map +1 -0
- package/dist/handlers/syndicate.d.ts +6 -0
- package/dist/handlers/syndicate.js +339 -0
- package/dist/handlers/syndicate.js.map +1 -0
- package/dist/handlers/team.d.ts +6 -0
- package/dist/handlers/team.js +1048 -0
- package/dist/handlers/team.js.map +1 -0
- package/dist/index.js +38 -4018
- package/dist/index.js.map +1 -1
- package/glama.json +6 -0
- package/package.json +1 -1
- package/src/handlers/advanced.ts +637 -0
- package/src/handlers/cache.ts +634 -0
- package/src/handlers/context.ts +218 -0
- package/src/handlers/fedbrain.ts +1143 -0
- package/src/handlers/instances.ts +180 -0
- package/src/handlers/roadmap.ts +192 -0
- package/src/handlers/syndicate.ts +451 -0
- package/src/handlers/team.ts +1163 -0
- package/src/index.ts +44 -4465
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
3
|
+
import { simpleHash } from '../confidence.js';
|
|
4
|
+
import type { Instance } from './brain.js';
|
|
5
|
+
|
|
6
|
+
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
7
|
+
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
8
|
+
|
|
9
|
+
export const CONTEXT_TOOL_NAMES = new Set([
|
|
10
|
+
'remember_context', 'recall_context', 'list_remembered', 'forget_context',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export async function handleContextTool(
|
|
14
|
+
name: string,
|
|
15
|
+
args: Record<string, unknown>,
|
|
16
|
+
getConnection: GetConnection,
|
|
17
|
+
apiFetch: ApiFetch,
|
|
18
|
+
): Promise<string | null> {
|
|
19
|
+
switch (name) {
|
|
20
|
+
case 'remember_context': {
|
|
21
|
+
const {
|
|
22
|
+
instance_id,
|
|
23
|
+
key,
|
|
24
|
+
content,
|
|
25
|
+
category = 'custom',
|
|
26
|
+
ttl = 86400,
|
|
27
|
+
} = args as {
|
|
28
|
+
instance_id: string;
|
|
29
|
+
key: string;
|
|
30
|
+
content: string;
|
|
31
|
+
category?: string;
|
|
32
|
+
ttl?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const redis = await getConnection(instance_id);
|
|
36
|
+
const cacheKey = `cachly:ctx:${category}:${key}`;
|
|
37
|
+
const meta = JSON.stringify({
|
|
38
|
+
key,
|
|
39
|
+
category,
|
|
40
|
+
size: content.length,
|
|
41
|
+
created: new Date().toISOString(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (ttl && ttl > 0) {
|
|
45
|
+
await redis.set(cacheKey, content, 'EX', ttl);
|
|
46
|
+
await redis.set(`${cacheKey}:meta`, meta, 'EX', ttl);
|
|
47
|
+
} else {
|
|
48
|
+
await redis.set(cacheKey, content);
|
|
49
|
+
await redis.set(`${cacheKey}:meta`, meta);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Also index semantically for smart_recall (if vector available)
|
|
53
|
+
const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
54
|
+
if (inst.vector_token) {
|
|
55
|
+
try {
|
|
56
|
+
const embedding = await computeEmbedding(`${key}: ${content.slice(0, 500)}`);
|
|
57
|
+
const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
58
|
+
const body: Record<string, unknown> = {
|
|
59
|
+
id: `ctx:${category}:${key}`,
|
|
60
|
+
prompt: key,
|
|
61
|
+
namespace: 'cachly:ctx',
|
|
62
|
+
embedding,
|
|
63
|
+
};
|
|
64
|
+
if (ttl && ttl > 0) {
|
|
65
|
+
body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
|
|
66
|
+
}
|
|
67
|
+
await fetch(`${vectorUrl}/entries`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
}).catch(() => undefined);
|
|
72
|
+
} catch {
|
|
73
|
+
// Embedding optional — continue silently
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [
|
|
78
|
+
`🧠 **Context Saved**`,
|
|
79
|
+
``,
|
|
80
|
+
` Key: \`${key}\``,
|
|
81
|
+
` Category: ${category}`,
|
|
82
|
+
` Size: ${content.length} chars`,
|
|
83
|
+
` TTL: ${ttl > 0 ? `${ttl}s (${Math.round(ttl / 3600)}h)` : 'no expiry'}`,
|
|
84
|
+
``,
|
|
85
|
+
`💡 Use \`recall_context("${key}")\` to retrieve this later.`,
|
|
86
|
+
` Or \`smart_recall("${key.split('_').join(' ')}")\` for semantic search.`,
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'recall_context': {
|
|
91
|
+
const { instance_id, key } = args as { instance_id: string; key: string };
|
|
92
|
+
const redis = await getConnection(instance_id);
|
|
93
|
+
|
|
94
|
+
// Check if key is a glob pattern
|
|
95
|
+
if (key.includes('*')) {
|
|
96
|
+
const keys: string[] = [];
|
|
97
|
+
const stream = redis.scanStream({ match: `cachly:ctx:*:${key}`, count: 100 });
|
|
98
|
+
await new Promise<void>((resolve, reject) => {
|
|
99
|
+
stream.on('data', (batch: string[]) => {
|
|
100
|
+
keys.push(...batch.filter((k: string) => !k.endsWith(':meta')));
|
|
101
|
+
if (keys.length >= 20) { stream.destroy(); resolve(); }
|
|
102
|
+
});
|
|
103
|
+
stream.on('end', resolve);
|
|
104
|
+
stream.on('error', reject);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (keys.length === 0) return `⚠️ No cached context found matching pattern \`${key}\`.`;
|
|
108
|
+
|
|
109
|
+
const results: string[] = [`🧠 **Recalled ${keys.length} context entries matching \`${key}\`:**\n`];
|
|
110
|
+
for (const k of keys.slice(0, 10)) {
|
|
111
|
+
const content = await redis.get(k);
|
|
112
|
+
const shortKey = k.replace('cachly:ctx:', '');
|
|
113
|
+
results.push(`### ${shortKey}\n\`\`\`\n${content?.slice(0, 500)}${(content?.length ?? 0) > 500 ? '…' : ''}\n\`\`\`\n`);
|
|
114
|
+
}
|
|
115
|
+
if (keys.length > 10) results.push(`_(+${keys.length - 10} more matches)_`);
|
|
116
|
+
return results.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Try exact match across categories
|
|
120
|
+
const categories = ['overview', 'architecture', 'file_summary', 'dependency', 'thinking', 'custom'];
|
|
121
|
+
for (const cat of categories) {
|
|
122
|
+
const content = await redis.get(`cachly:ctx:${cat}:${key}`);
|
|
123
|
+
if (content) {
|
|
124
|
+
const ttl = await redis.ttl(`cachly:ctx:${cat}:${key}`);
|
|
125
|
+
return [
|
|
126
|
+
`🧠 **Recalled Context: \`${key}\`**`,
|
|
127
|
+
``,
|
|
128
|
+
` Category: ${cat}`,
|
|
129
|
+
` Size: ${content.length} chars`,
|
|
130
|
+
` TTL: ${ttl === -1 ? 'no expiry' : ttl === -2 ? 'expired' : `${ttl}s remaining`}`,
|
|
131
|
+
``,
|
|
132
|
+
`---`,
|
|
133
|
+
``,
|
|
134
|
+
content,
|
|
135
|
+
].join('\n');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `⚠️ No cached context found for key \`${key}\`.\n\nUse \`list_remembered\` to see available cached context.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'list_remembered': {
|
|
143
|
+
const {
|
|
144
|
+
instance_id,
|
|
145
|
+
category = 'all',
|
|
146
|
+
limit = 50,
|
|
147
|
+
} = args as { instance_id: string; category?: string; limit?: number };
|
|
148
|
+
|
|
149
|
+
const redis = await getConnection(instance_id);
|
|
150
|
+
const pattern = category === 'all' ? 'cachly:ctx:*' : `cachly:ctx:${category}:*`;
|
|
151
|
+
const keys: string[] = [];
|
|
152
|
+
const stream = redis.scanStream({ match: pattern, count: 100 });
|
|
153
|
+
await new Promise<void>((resolve, reject) => {
|
|
154
|
+
stream.on('data', (batch: string[]) => {
|
|
155
|
+
keys.push(...batch.filter((k: string) => !k.endsWith(':meta')));
|
|
156
|
+
if (keys.length >= limit) { stream.destroy(); resolve(); }
|
|
157
|
+
});
|
|
158
|
+
stream.on('end', resolve);
|
|
159
|
+
stream.on('error', reject);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (keys.length === 0) {
|
|
163
|
+
return `📭 No cached context found.\n\nUse \`remember_context\` to cache context for faster future access.`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const lines: string[] = [`🧠 **Cached Context** (${keys.length} entries):\n`];
|
|
167
|
+
for (const k of keys.slice(0, limit)) {
|
|
168
|
+
const ttl = await redis.ttl(k);
|
|
169
|
+
const content = await redis.get(k);
|
|
170
|
+
const parts = k.replace('cachly:ctx:', '').split(':');
|
|
171
|
+
const cat = parts[0];
|
|
172
|
+
const key = parts.slice(1).join(':');
|
|
173
|
+
const preview = content?.slice(0, 80).replace(/\n/g, ' ') ?? '';
|
|
174
|
+
lines.push(
|
|
175
|
+
` • **${key}** (${cat})`,
|
|
176
|
+
` Size: ${content?.length ?? 0} chars · TTL: ${ttl === -1 ? '∞' : `${Math.round(ttl / 60)}m`}`,
|
|
177
|
+
` _"${preview}${(content?.length ?? 0) > 80 ? '…' : ''}"_`,
|
|
178
|
+
``
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case 'forget_context': {
|
|
186
|
+
const { instance_id, keys } = args as { instance_id: string; keys: string[] };
|
|
187
|
+
const redis = await getConnection(instance_id);
|
|
188
|
+
let deleted = 0;
|
|
189
|
+
|
|
190
|
+
for (const key of keys) {
|
|
191
|
+
if (key.includes('*')) {
|
|
192
|
+
// Glob delete
|
|
193
|
+
const toDelete: string[] = [];
|
|
194
|
+
const stream = redis.scanStream({ match: `cachly:ctx:*:${key}*`, count: 100 });
|
|
195
|
+
await new Promise<void>((resolve, reject) => {
|
|
196
|
+
stream.on('data', (batch: string[]) => toDelete.push(...batch));
|
|
197
|
+
stream.on('end', resolve);
|
|
198
|
+
stream.on('error', reject);
|
|
199
|
+
});
|
|
200
|
+
if (toDelete.length > 0) {
|
|
201
|
+
deleted += await redis.del(...toDelete);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
// Try all categories
|
|
205
|
+
const categories = ['overview', 'architecture', 'file_summary', 'dependency', 'thinking', 'custom'];
|
|
206
|
+
for (const cat of categories) {
|
|
207
|
+
deleted += await redis.del(`cachly:ctx:${cat}:${key}`, `cachly:ctx:${cat}:${key}:meta`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return `🗑️ **Forgot ${deleted} context entries.**\n\nKeys: ${keys.map(k => `\`${k}\``).join(', ')}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
default:
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|