@usewhisper/mcp-server 0.2.3 → 0.3.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 +23 -23
- package/dist/autosubscribe-GHO6YR5A.js +4068 -0
- package/dist/chunk-52VJYCZ7.js +455 -0
- package/dist/chunk-5KBZQHDL.js +189 -0
- package/dist/chunk-7SN3CKDK.js +1076 -0
- package/dist/chunk-EI5CE3EY.js +616 -0
- package/dist/chunk-JO3ORBZD.js +616 -0
- package/dist/chunk-LMEYV4JD.js +368 -0
- package/dist/chunk-MEFLJ4PV.js +8385 -0
- package/dist/chunk-PPGYJJED.js +271 -0
- package/dist/chunk-T7KMSTWP.js +399 -0
- package/dist/chunk-TWEIYHI6.js +399 -0
- package/dist/consolidation-2GCKI4RE.js +220 -0
- package/dist/consolidation-4JOPW6BG.js +220 -0
- package/dist/context-sharing-4ITCNKG4.js +307 -0
- package/dist/context-sharing-GYKLXHZA.js +307 -0
- package/dist/context-sharing-Y6LTZZOF.js +307 -0
- package/dist/cost-optimization-7DVSTL6R.js +307 -0
- package/dist/ingest-7T5FAZNC.js +15 -0
- package/dist/ingest-EBNIE7XB.js +15 -0
- package/dist/ingest-FSHT5BCS.js +15 -0
- package/dist/oracle-3RLQF3DP.js +259 -0
- package/dist/oracle-FKRTQUUG.js +282 -0
- package/dist/search-EG6TYWWW.js +13 -0
- package/dist/search-I22QQA7T.js +13 -0
- package/dist/search-T7H5G6DW.js +13 -0
- package/dist/server.js +813 -1088
- package/package.json +2 -6
package/dist/server.js
CHANGED
|
@@ -1,801 +1,629 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ingestDocument
|
|
4
|
-
} from "./chunk-X7HNNNJJ.js";
|
|
5
|
-
import {
|
|
6
|
-
embedSingle,
|
|
7
|
-
prisma
|
|
8
|
-
} from "./chunk-3WGYBAYR.js";
|
|
9
|
-
import "./chunk-QGM4M3NI.js";
|
|
10
2
|
|
|
11
3
|
// ../src/mcp/server.ts
|
|
12
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
6
|
import { z } from "zod";
|
|
15
7
|
|
|
16
|
-
// ../src/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
previousContextHash,
|
|
30
|
-
previousContext,
|
|
31
|
-
targetReduction = 0.5
|
|
32
|
-
} = opts;
|
|
33
|
-
const originalTokens = estimateTokens(rawContext);
|
|
34
|
-
if (originalTokens <= maxTokens) {
|
|
35
|
-
return {
|
|
36
|
-
context: rawContext,
|
|
37
|
-
originalTokens,
|
|
38
|
-
compressedTokens: originalTokens,
|
|
39
|
-
reductionPercent: 0,
|
|
40
|
-
strategy: "none"
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
switch (strategy) {
|
|
44
|
-
case "delta":
|
|
45
|
-
return deltaCompress(rawContext, originalTokens, maxTokens, previousContextHash, previousContext);
|
|
46
|
-
case "summarize":
|
|
47
|
-
return summarizeCompress(rawContext, originalTokens, maxTokens);
|
|
48
|
-
case "extract":
|
|
49
|
-
return extractCompress(rawContext, originalTokens, maxTokens);
|
|
50
|
-
case "adaptive":
|
|
51
|
-
default:
|
|
52
|
-
return adaptiveCompress(rawContext, originalTokens, maxTokens, previousContextHash, previousContext);
|
|
8
|
+
// ../src/sdk/index.ts
|
|
9
|
+
var WhisperError = class extends Error {
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
retryable;
|
|
13
|
+
details;
|
|
14
|
+
constructor(args) {
|
|
15
|
+
super(args.message);
|
|
16
|
+
this.name = "WhisperError";
|
|
17
|
+
this.code = args.code;
|
|
18
|
+
this.status = args.status;
|
|
19
|
+
this.retryable = args.retryable ?? false;
|
|
20
|
+
this.details = args.details;
|
|
53
21
|
}
|
|
22
|
+
};
|
|
23
|
+
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
24
|
+
var DEFAULT_BASE_DELAY_MS = 250;
|
|
25
|
+
var DEFAULT_MAX_DELAY_MS = 2e3;
|
|
26
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
27
|
+
var PROJECT_CACHE_TTL_MS = 3e4;
|
|
28
|
+
function sleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
30
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const delta = await deltaCompress(rawContext, originalTokens, maxTokens, previousHash, previousCtx);
|
|
59
|
-
if (delta.compressedTokens <= maxTokens) return delta;
|
|
60
|
-
}
|
|
61
|
-
if (ratio < 2) {
|
|
62
|
-
return extractCompress(rawContext, originalTokens, maxTokens);
|
|
63
|
-
}
|
|
64
|
-
return summarizeCompress(rawContext, originalTokens, maxTokens);
|
|
31
|
+
function getBackoffDelay(attempt, base, max) {
|
|
32
|
+
const jitter = 0.8 + Math.random() * 0.4;
|
|
33
|
+
return Math.min(max, Math.floor(base * Math.pow(2, attempt) * jitter));
|
|
65
34
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
35
|
+
function isLikelyProjectId(projectRef) {
|
|
36
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(projectRef);
|
|
37
|
+
}
|
|
38
|
+
var WhisperContext = class _WhisperContext {
|
|
39
|
+
apiKey;
|
|
40
|
+
baseUrl;
|
|
41
|
+
defaultProject;
|
|
42
|
+
orgId;
|
|
43
|
+
timeoutMs;
|
|
44
|
+
retryConfig;
|
|
45
|
+
projectRefToId = /* @__PURE__ */ new Map();
|
|
46
|
+
projectCache = [];
|
|
47
|
+
projectCacheExpiresAt = 0;
|
|
48
|
+
constructor(config) {
|
|
49
|
+
if (!config.apiKey) {
|
|
50
|
+
throw new WhisperError({
|
|
51
|
+
code: "INVALID_API_KEY",
|
|
52
|
+
message: "API key is required"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
this.apiKey = config.apiKey;
|
|
56
|
+
this.baseUrl = config.baseUrl || "https://context.usewhisper.dev";
|
|
57
|
+
this.defaultProject = config.project;
|
|
58
|
+
this.orgId = config.orgId;
|
|
59
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
60
|
+
this.retryConfig = {
|
|
61
|
+
maxAttempts: config.retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
62
|
+
baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS,
|
|
63
|
+
maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS
|
|
75
64
|
};
|
|
76
65
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
withProject(project) {
|
|
67
|
+
return new _WhisperContext({
|
|
68
|
+
apiKey: this.apiKey,
|
|
69
|
+
baseUrl: this.baseUrl,
|
|
70
|
+
project,
|
|
71
|
+
orgId: this.orgId,
|
|
72
|
+
timeoutMs: this.timeoutMs,
|
|
73
|
+
retry: this.retryConfig
|
|
74
|
+
});
|
|
83
75
|
}
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
getRequiredProject(project) {
|
|
77
|
+
const resolved = project || this.defaultProject;
|
|
78
|
+
if (!resolved) {
|
|
79
|
+
throw new WhisperError({
|
|
80
|
+
code: "MISSING_PROJECT",
|
|
81
|
+
message: "Project is required. Pass project in params or set a default project in WhisperContext config."
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return resolved;
|
|
86
85
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const unchangedCount = { count: 0 };
|
|
91
|
-
for (const block of currentBlocks) {
|
|
92
|
-
if (prevBlocks.has(block)) {
|
|
93
|
-
unchangedCount.count++;
|
|
94
|
-
} else {
|
|
95
|
-
newBlocks.push(block);
|
|
86
|
+
async refreshProjectCache(force = false) {
|
|
87
|
+
if (!force && Date.now() < this.projectCacheExpiresAt && this.projectCache.length > 0) {
|
|
88
|
+
return this.projectCache;
|
|
96
89
|
}
|
|
90
|
+
const response = await this.request("/v1/projects", { method: "GET" });
|
|
91
|
+
this.projectRefToId.clear();
|
|
92
|
+
this.projectCache = response.projects || [];
|
|
93
|
+
for (const p of this.projectCache) {
|
|
94
|
+
this.projectRefToId.set(p.id, p.id);
|
|
95
|
+
this.projectRefToId.set(p.slug, p.id);
|
|
96
|
+
this.projectRefToId.set(p.name, p.id);
|
|
97
|
+
}
|
|
98
|
+
this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
|
|
99
|
+
return this.projectCache;
|
|
97
100
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
async resolveProjectId(projectRef) {
|
|
102
|
+
if (this.projectRefToId.has(projectRef)) {
|
|
103
|
+
return this.projectRefToId.get(projectRef);
|
|
104
|
+
}
|
|
105
|
+
const projects = await this.refreshProjectCache(true);
|
|
106
|
+
const byDirect = projects.find((p) => p.id === projectRef);
|
|
107
|
+
if (byDirect) return byDirect.id;
|
|
108
|
+
const matches = projects.filter((p) => p.slug === projectRef || p.name === projectRef);
|
|
109
|
+
if (matches.length === 1) {
|
|
110
|
+
return matches[0].id;
|
|
111
|
+
}
|
|
112
|
+
if (matches.length > 1) {
|
|
113
|
+
throw new WhisperError({
|
|
114
|
+
code: "PROJECT_AMBIGUOUS",
|
|
115
|
+
message: `Project reference '${projectRef}' matched multiple projects. Use project id instead.`
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (isLikelyProjectId(projectRef)) {
|
|
119
|
+
return projectRef;
|
|
120
|
+
}
|
|
121
|
+
throw new WhisperError({
|
|
122
|
+
code: "PROJECT_NOT_FOUND",
|
|
123
|
+
message: `Project '${projectRef}' not found`
|
|
124
|
+
});
|
|
106
125
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
async getProjectRefCandidates(projectRef) {
|
|
127
|
+
const candidates = /* @__PURE__ */ new Set([projectRef]);
|
|
128
|
+
try {
|
|
129
|
+
const projects = await this.refreshProjectCache(false);
|
|
130
|
+
const match = projects.find((p) => p.id === projectRef || p.slug === projectRef || p.name === projectRef);
|
|
131
|
+
if (match) {
|
|
132
|
+
candidates.add(match.id);
|
|
133
|
+
candidates.add(match.slug);
|
|
134
|
+
candidates.add(match.name);
|
|
135
|
+
} else if (isLikelyProjectId(projectRef)) {
|
|
136
|
+
const byId = projects.find((p) => p.id === projectRef);
|
|
137
|
+
if (byId) {
|
|
138
|
+
candidates.add(byId.slug);
|
|
139
|
+
candidates.add(byId.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
return Array.from(candidates).filter(Boolean);
|
|
118
145
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
],
|
|
140
|
-
max_tokens: maxTokens,
|
|
141
|
-
temperature: 0
|
|
146
|
+
async withProjectRefFallback(projectRef, execute) {
|
|
147
|
+
const refs = await this.getProjectRefCandidates(projectRef);
|
|
148
|
+
let lastError;
|
|
149
|
+
for (const ref of refs) {
|
|
150
|
+
try {
|
|
151
|
+
return await execute(ref);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
lastError = error;
|
|
154
|
+
if (error instanceof WhisperError && error.code === "PROJECT_NOT_FOUND") {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (lastError instanceof Error) {
|
|
161
|
+
throw lastError;
|
|
162
|
+
}
|
|
163
|
+
throw new WhisperError({
|
|
164
|
+
code: "PROJECT_NOT_FOUND",
|
|
165
|
+
message: `Project '${projectRef}' not found`
|
|
142
166
|
});
|
|
143
|
-
const compressed = res.choices[0]?.message?.content?.trim() || rawContext;
|
|
144
|
-
const compressedTokens = estimateTokens(compressed);
|
|
145
|
-
return {
|
|
146
|
-
context: compressed,
|
|
147
|
-
originalTokens,
|
|
148
|
-
compressedTokens,
|
|
149
|
-
reductionPercent: Math.round((1 - compressedTokens / originalTokens) * 100),
|
|
150
|
-
strategy: "extract"
|
|
151
|
-
};
|
|
152
|
-
} catch {
|
|
153
|
-
const truncated = truncateToTokens(rawContext, maxTokens);
|
|
154
|
-
return {
|
|
155
|
-
context: truncated,
|
|
156
|
-
originalTokens,
|
|
157
|
-
compressedTokens: estimateTokens(truncated),
|
|
158
|
-
reductionPercent: Math.round((1 - estimateTokens(truncated) / originalTokens) * 100),
|
|
159
|
-
strategy: "truncate-fallback"
|
|
160
|
-
};
|
|
161
167
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
classifyError(status, message) {
|
|
169
|
+
if (status === 401 || /api key|unauthorized|forbidden/i.test(message)) {
|
|
170
|
+
return { code: "INVALID_API_KEY", retryable: false };
|
|
171
|
+
}
|
|
172
|
+
if (status === 404 || /project not found/i.test(message)) {
|
|
173
|
+
return { code: "PROJECT_NOT_FOUND", retryable: false };
|
|
174
|
+
}
|
|
175
|
+
if (status === 408) {
|
|
176
|
+
return { code: "TIMEOUT", retryable: true };
|
|
177
|
+
}
|
|
178
|
+
if (status === 429) {
|
|
179
|
+
return { code: "RATE_LIMITED", retryable: true };
|
|
180
|
+
}
|
|
181
|
+
if (status !== void 0 && status >= 500) {
|
|
182
|
+
return { code: "TEMPORARY_UNAVAILABLE", retryable: true };
|
|
183
|
+
}
|
|
184
|
+
return { code: "REQUEST_FAILED", retryable: false };
|
|
167
185
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
temperature: 0
|
|
186
|
+
async request(endpoint, options = {}) {
|
|
187
|
+
const maxAttempts = Math.max(1, this.retryConfig.maxAttempts);
|
|
188
|
+
let lastError;
|
|
189
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
194
|
+
...options,
|
|
195
|
+
signal: controller.signal,
|
|
196
|
+
headers: {
|
|
197
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
...this.orgId ? { "X-Whisper-Org-Id": this.orgId } : {},
|
|
200
|
+
...options.headers
|
|
201
|
+
}
|
|
185
202
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
clearTimeout(timeout);
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
let payload = null;
|
|
206
|
+
try {
|
|
207
|
+
payload = await response.json();
|
|
208
|
+
} catch {
|
|
209
|
+
payload = await response.text().catch(() => "");
|
|
210
|
+
}
|
|
211
|
+
const message = typeof payload === "string" ? payload : payload?.error || payload?.message || `HTTP ${response.status}: ${response.statusText}`;
|
|
212
|
+
const { code, retryable } = this.classifyError(response.status, message);
|
|
213
|
+
const err = new WhisperError({
|
|
214
|
+
code,
|
|
215
|
+
message,
|
|
216
|
+
status: response.status,
|
|
217
|
+
retryable,
|
|
218
|
+
details: payload
|
|
219
|
+
});
|
|
220
|
+
if (!retryable || attempt === maxAttempts - 1) {
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
await sleep(getBackoffDelay(attempt, this.retryConfig.baseDelayMs, this.retryConfig.maxDelayMs));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
return response.json();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
clearTimeout(timeout);
|
|
229
|
+
const isAbort = error?.name === "AbortError";
|
|
230
|
+
const mapped = error instanceof WhisperError ? error : new WhisperError({
|
|
231
|
+
code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
|
|
232
|
+
message: isAbort ? "Request timed out" : error?.message || "Network request failed",
|
|
233
|
+
retryable: true,
|
|
234
|
+
details: error
|
|
235
|
+
});
|
|
236
|
+
lastError = mapped;
|
|
237
|
+
if (!mapped.retryable || attempt === maxAttempts - 1) {
|
|
238
|
+
throw mapped;
|
|
239
|
+
}
|
|
240
|
+
await sleep(getBackoffDelay(attempt, this.retryConfig.baseDelayMs, this.retryConfig.maxDelayMs));
|
|
241
|
+
}
|
|
195
242
|
}
|
|
196
|
-
|
|
197
|
-
context: finalContext,
|
|
198
|
-
originalTokens,
|
|
199
|
-
compressedTokens,
|
|
200
|
-
reductionPercent: Math.round((1 - compressedTokens / originalTokens) * 100),
|
|
201
|
-
strategy: "summarize"
|
|
202
|
-
};
|
|
203
|
-
} catch {
|
|
204
|
-
const truncated = truncateToTokens(rawContext, maxTokens);
|
|
205
|
-
return {
|
|
206
|
-
context: truncated,
|
|
207
|
-
originalTokens,
|
|
208
|
-
compressedTokens: estimateTokens(truncated),
|
|
209
|
-
reductionPercent: Math.round((1 - estimateTokens(truncated) / originalTokens) * 100),
|
|
210
|
-
strategy: "truncate-fallback"
|
|
211
|
-
};
|
|
243
|
+
throw lastError instanceof Error ? lastError : new WhisperError({ code: "REQUEST_FAILED", message: "Request failed" });
|
|
212
244
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
245
|
+
async query(params) {
|
|
246
|
+
const projectRef = this.getRequiredProject(params.project);
|
|
247
|
+
return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/context/query", {
|
|
248
|
+
method: "POST",
|
|
249
|
+
body: JSON.stringify({ ...params, project })
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
async createProject(params) {
|
|
253
|
+
const project = await this.request("/v1/projects", {
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: JSON.stringify(params)
|
|
256
|
+
});
|
|
257
|
+
this.projectRefToId.set(project.id, project.id);
|
|
258
|
+
this.projectRefToId.set(project.slug, project.id);
|
|
259
|
+
this.projectRefToId.set(project.name, project.id);
|
|
260
|
+
this.projectCache = [
|
|
261
|
+
...this.projectCache.filter((p) => p.id !== project.id),
|
|
262
|
+
project
|
|
263
|
+
];
|
|
264
|
+
this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
|
|
265
|
+
return project;
|
|
266
|
+
}
|
|
267
|
+
async listProjects() {
|
|
268
|
+
const projects = await this.request("/v1/projects", { method: "GET" });
|
|
269
|
+
this.projectCache = projects.projects || [];
|
|
270
|
+
for (const p of projects.projects || []) {
|
|
271
|
+
this.projectRefToId.set(p.id, p.id);
|
|
272
|
+
this.projectRefToId.set(p.slug, p.id);
|
|
273
|
+
this.projectRefToId.set(p.name, p.id);
|
|
229
274
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
function stopCacheCleanup() {
|
|
233
|
-
if (cacheCleanupInterval) {
|
|
234
|
-
clearInterval(cacheCleanupInterval);
|
|
235
|
-
cacheCleanupInterval = null;
|
|
275
|
+
this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
|
|
276
|
+
return projects;
|
|
236
277
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
278
|
+
async getProject(id) {
|
|
279
|
+
const projectId = await this.resolveProjectId(id);
|
|
280
|
+
return this.request(`/v1/projects/${projectId}`);
|
|
281
|
+
}
|
|
282
|
+
async deleteProject(id) {
|
|
283
|
+
const projectId = await this.resolveProjectId(id);
|
|
284
|
+
return this.request(`/v1/projects/${projectId}`, { method: "DELETE" });
|
|
285
|
+
}
|
|
286
|
+
async addSource(projectId, params) {
|
|
287
|
+
const resolvedProjectId = await this.resolveProjectId(projectId);
|
|
288
|
+
return this.request(`/v1/projects/${resolvedProjectId}/sources`, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
body: JSON.stringify(params)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async syncSource(sourceId) {
|
|
294
|
+
return this.request(`/v1/sources/${sourceId}/sync`, { method: "POST" });
|
|
295
|
+
}
|
|
296
|
+
async ingest(projectId, documents) {
|
|
297
|
+
const resolvedProjectId = await this.resolveProjectId(projectId);
|
|
298
|
+
return this.request(`/v1/projects/${resolvedProjectId}/ingest`, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
body: JSON.stringify({ documents })
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async addContext(params) {
|
|
304
|
+
const projectId = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
305
|
+
return this.ingest(projectId, [
|
|
306
|
+
{
|
|
307
|
+
title: params.title || "Context",
|
|
308
|
+
content: params.content,
|
|
309
|
+
metadata: params.metadata || { source: "addContext" }
|
|
310
|
+
}
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
async addMemory(params) {
|
|
314
|
+
const projectRef = this.getRequiredProject(params.project);
|
|
315
|
+
return this.withProjectRefFallback(projectRef, async (project) => {
|
|
316
|
+
const toSotaType = (memoryType) => {
|
|
317
|
+
switch (memoryType) {
|
|
318
|
+
case "episodic":
|
|
319
|
+
return "event";
|
|
320
|
+
case "semantic":
|
|
321
|
+
return "factual";
|
|
322
|
+
case "procedural":
|
|
323
|
+
return "instruction";
|
|
324
|
+
default:
|
|
325
|
+
return memoryType;
|
|
285
326
|
}
|
|
286
327
|
};
|
|
287
|
-
|
|
328
|
+
const toLegacyType = (memoryType) => {
|
|
329
|
+
switch (memoryType) {
|
|
330
|
+
case "event":
|
|
331
|
+
return "episodic";
|
|
332
|
+
case "instruction":
|
|
333
|
+
return "procedural";
|
|
334
|
+
case "preference":
|
|
335
|
+
case "relationship":
|
|
336
|
+
case "opinion":
|
|
337
|
+
case "goal":
|
|
338
|
+
return "semantic";
|
|
339
|
+
default:
|
|
340
|
+
return memoryType;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
try {
|
|
344
|
+
const direct = await this.request("/v1/memory", {
|
|
345
|
+
method: "POST",
|
|
346
|
+
body: JSON.stringify({
|
|
347
|
+
project,
|
|
348
|
+
content: params.content,
|
|
349
|
+
memory_type: toSotaType(params.memory_type),
|
|
350
|
+
user_id: params.user_id,
|
|
351
|
+
session_id: params.session_id,
|
|
352
|
+
agent_id: params.agent_id,
|
|
353
|
+
importance: params.importance,
|
|
354
|
+
metadata: params.metadata
|
|
355
|
+
})
|
|
356
|
+
});
|
|
357
|
+
const id2 = direct?.memory?.id || direct?.id || direct?.memory_id;
|
|
358
|
+
if (id2) {
|
|
359
|
+
return { id: id2, success: true, path: "sota", fallback_used: false };
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (params.allow_legacy_fallback === false) {
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const legacy = await this.request("/v1/memories", {
|
|
367
|
+
method: "POST",
|
|
368
|
+
body: JSON.stringify({
|
|
369
|
+
project,
|
|
370
|
+
content: params.content,
|
|
371
|
+
memory_type: toLegacyType(params.memory_type),
|
|
372
|
+
user_id: params.user_id,
|
|
373
|
+
session_id: params.session_id,
|
|
374
|
+
agent_id: params.agent_id,
|
|
375
|
+
importance: params.importance,
|
|
376
|
+
metadata: params.metadata,
|
|
377
|
+
expires_in_seconds: params.expires_in_seconds
|
|
378
|
+
})
|
|
379
|
+
});
|
|
380
|
+
const id = legacy?.memory?.id || legacy?.id || legacy?.memory_id;
|
|
381
|
+
if (!id) {
|
|
382
|
+
throw new WhisperError({
|
|
383
|
+
code: "REQUEST_FAILED",
|
|
384
|
+
message: "Memory create succeeded but no memory id was returned by the API"
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return { id, success: true, path: "legacy", fallback_used: true };
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
async searchMemories(params) {
|
|
391
|
+
const projectRef = this.getRequiredProject(params.project);
|
|
392
|
+
return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
|
|
393
|
+
method: "POST",
|
|
394
|
+
body: JSON.stringify({
|
|
395
|
+
query: params.query,
|
|
396
|
+
project,
|
|
397
|
+
user_id: params.user_id,
|
|
398
|
+
session_id: params.session_id,
|
|
399
|
+
memory_types: params.memory_type ? [params.memory_type] : void 0,
|
|
400
|
+
top_k: params.top_k || 10
|
|
401
|
+
})
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
async createApiKey(params) {
|
|
405
|
+
return this.request("/v1/keys", {
|
|
406
|
+
method: "POST",
|
|
407
|
+
body: JSON.stringify(params)
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async listApiKeys() {
|
|
411
|
+
return this.request("/v1/keys");
|
|
412
|
+
}
|
|
413
|
+
async getUsage(days = 30) {
|
|
414
|
+
return this.request(`/v1/usage?days=${days}`);
|
|
288
415
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const bm25Results = await fullTextSearch(projectId, query, maxResultsPerSearch, chunkTypes);
|
|
296
|
-
allResults.push(...bm25Results);
|
|
416
|
+
async searchMemoriesSOTA(params) {
|
|
417
|
+
const projectRef = this.getRequiredProject(params.project);
|
|
418
|
+
return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
|
|
419
|
+
method: "POST",
|
|
420
|
+
body: JSON.stringify({ ...params, project })
|
|
421
|
+
}));
|
|
297
422
|
}
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
423
|
+
async ingestSession(params) {
|
|
424
|
+
const projectRef = this.getRequiredProject(params.project);
|
|
425
|
+
return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/ingest/session", {
|
|
426
|
+
method: "POST",
|
|
427
|
+
body: JSON.stringify({ ...params, project })
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
async getSessionMemories(params) {
|
|
431
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
432
|
+
const query = new URLSearchParams({
|
|
433
|
+
project,
|
|
434
|
+
...params.limit && { limit: params.limit.toString() },
|
|
435
|
+
...params.since_date && { since_date: params.since_date }
|
|
304
436
|
});
|
|
305
|
-
|
|
437
|
+
return this.request(`/v1/memory/session/${params.session_id}?${query}`);
|
|
306
438
|
}
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
439
|
+
async getUserProfile(params) {
|
|
440
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
441
|
+
const query = new URLSearchParams({
|
|
442
|
+
project,
|
|
443
|
+
...params.memory_types && { memory_types: params.memory_types }
|
|
311
444
|
});
|
|
312
|
-
|
|
445
|
+
return this.request(`/v1/memory/profile/${params.user_id}?${query}`);
|
|
446
|
+
}
|
|
447
|
+
async getMemoryVersions(memoryId) {
|
|
448
|
+
return this.request(`/v1/memory/${memoryId}/versions`);
|
|
313
449
|
}
|
|
314
|
-
|
|
315
|
-
|
|
450
|
+
async updateMemory(memoryId, params) {
|
|
451
|
+
return this.request(`/v1/memory/${memoryId}`, {
|
|
452
|
+
method: "PUT",
|
|
453
|
+
body: JSON.stringify(params)
|
|
454
|
+
});
|
|
316
455
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
allResults = reciprocalRankFusion(allResults, vectorWeight, bm25Weight);
|
|
456
|
+
async deleteMemory(memoryId) {
|
|
457
|
+
return this.request(`/v1/memory/${memoryId}`, { method: "DELETE" });
|
|
320
458
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const reranked = await rerankResults(query, allResults, rerankTopK || topK);
|
|
324
|
-
allResults = reranked;
|
|
459
|
+
async getMemoryRelations(memoryId) {
|
|
460
|
+
return this.request(`/v1/memory/${memoryId}/relations`);
|
|
325
461
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (compress && context.length > 0) {
|
|
332
|
-
const compressed = await compressContext(context, {
|
|
333
|
-
maxTokens: maxTokens || 4e3,
|
|
334
|
-
strategy: compressionStrategy,
|
|
335
|
-
previousContextHash
|
|
462
|
+
async oracleSearch(params) {
|
|
463
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
464
|
+
return this.request("/v1/oracle/search", {
|
|
465
|
+
method: "POST",
|
|
466
|
+
body: JSON.stringify({ ...params, project })
|
|
336
467
|
});
|
|
337
|
-
context = compressed.context;
|
|
338
|
-
compressionMeta = {
|
|
339
|
-
originalTokens: compressed.originalTokens,
|
|
340
|
-
compressedTokens: compressed.compressedTokens,
|
|
341
|
-
reductionPercent: compressed.reductionPercent,
|
|
342
|
-
strategy: compressed.strategy
|
|
343
|
-
};
|
|
344
468
|
}
|
|
345
|
-
|
|
346
|
-
|
|
469
|
+
async autosubscribe(params) {
|
|
470
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
471
|
+
return this.request("/v1/autosubscribe", {
|
|
472
|
+
method: "POST",
|
|
473
|
+
body: JSON.stringify({ ...params, project })
|
|
474
|
+
});
|
|
347
475
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
latencyMs,
|
|
355
|
-
cacheHit: false,
|
|
356
|
-
tokensUsed: estimateTokens2(context),
|
|
357
|
-
contextHash,
|
|
358
|
-
compression: compressionMeta
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
async function vectorSearch(projectId, queryEmbedding, limit, chunkTypes) {
|
|
363
|
-
const embeddingStr = `[${queryEmbedding.join(",")}]`;
|
|
364
|
-
const results = chunkTypes && chunkTypes.length > 0 ? await prisma.$queryRaw`
|
|
365
|
-
SELECT
|
|
366
|
-
id, content, "chunkType", metadata,
|
|
367
|
-
1 - (embedding <=> ${embeddingStr}::vector) as similarity
|
|
368
|
-
FROM chunks
|
|
369
|
-
WHERE "projectId" = ${projectId}
|
|
370
|
-
AND "chunkType" = ANY(${chunkTypes})
|
|
371
|
-
ORDER BY embedding <=> ${embeddingStr}::vector
|
|
372
|
-
LIMIT ${limit}
|
|
373
|
-
` : await prisma.$queryRaw`
|
|
374
|
-
SELECT
|
|
375
|
-
id, content, "chunkType", metadata,
|
|
376
|
-
1 - (embedding <=> ${embeddingStr}::vector) as similarity
|
|
377
|
-
FROM chunks
|
|
378
|
-
WHERE "projectId" = ${projectId}
|
|
379
|
-
ORDER BY embedding <=> ${embeddingStr}::vector
|
|
380
|
-
LIMIT ${limit}
|
|
381
|
-
`;
|
|
382
|
-
return results.map((r) => ({
|
|
383
|
-
id: r.id,
|
|
384
|
-
content: r.content,
|
|
385
|
-
score: r.similarity,
|
|
386
|
-
metadata: r.metadata || {},
|
|
387
|
-
chunkType: r.chunkType,
|
|
388
|
-
source: "vector"
|
|
389
|
-
}));
|
|
390
|
-
}
|
|
391
|
-
async function fullTextSearch(projectId, query, limit, chunkTypes) {
|
|
392
|
-
const tsQuery = query.replace(/[^\w\s]/g, " ").trim().split(/\s+/).filter((w) => w.length > 1).join(" & ");
|
|
393
|
-
if (!tsQuery) return [];
|
|
394
|
-
const results = chunkTypes && chunkTypes.length > 0 ? await prisma.$queryRaw`
|
|
395
|
-
SELECT
|
|
396
|
-
id, content, "chunkType", metadata,
|
|
397
|
-
ts_rank(to_tsvector('english', coalesce("searchContent", content)), to_tsquery('english', ${tsQuery})) as rank
|
|
398
|
-
FROM chunks
|
|
399
|
-
WHERE "projectId" = ${projectId}
|
|
400
|
-
AND "chunkType" = ANY(${chunkTypes})
|
|
401
|
-
AND to_tsvector('english', coalesce("searchContent", content)) @@ to_tsquery('english', ${tsQuery})
|
|
402
|
-
ORDER BY rank DESC
|
|
403
|
-
LIMIT ${limit}
|
|
404
|
-
` : await prisma.$queryRaw`
|
|
405
|
-
SELECT
|
|
406
|
-
id, content, "chunkType", metadata,
|
|
407
|
-
ts_rank(to_tsvector('english', coalesce("searchContent", content)), to_tsquery('english', ${tsQuery})) as rank
|
|
408
|
-
FROM chunks
|
|
409
|
-
WHERE "projectId" = ${projectId}
|
|
410
|
-
AND to_tsvector('english', coalesce("searchContent", content)) @@ to_tsquery('english', ${tsQuery})
|
|
411
|
-
ORDER BY rank DESC
|
|
412
|
-
LIMIT ${limit}
|
|
413
|
-
`;
|
|
414
|
-
const maxRank = results.length > 0 ? Math.max(...results.map((r) => r.rank)) : 1;
|
|
415
|
-
return results.map((r) => ({
|
|
416
|
-
id: r.id,
|
|
417
|
-
content: r.content,
|
|
418
|
-
score: maxRank > 0 ? r.rank / maxRank : 0,
|
|
419
|
-
metadata: r.metadata || {},
|
|
420
|
-
chunkType: r.chunkType,
|
|
421
|
-
source: "bm25"
|
|
422
|
-
}));
|
|
423
|
-
}
|
|
424
|
-
async function memorySearch(projectId, queryEmbedding, opts) {
|
|
425
|
-
const embeddingStr = `[${queryEmbedding.join(",")}]`;
|
|
426
|
-
let query;
|
|
427
|
-
if (opts.userId && opts.sessionId && opts.agentId) {
|
|
428
|
-
query = prisma.$queryRaw`
|
|
429
|
-
SELECT
|
|
430
|
-
id, content, memory_type as "memoryType", metadata, importance,
|
|
431
|
-
1 - (embedding <=> ${embeddingStr}::vector) as similarity
|
|
432
|
-
FROM memories
|
|
433
|
-
WHERE project_id = ${projectId}
|
|
434
|
-
AND is_active = true
|
|
435
|
-
AND (expires_at IS NULL OR expires_at > NOW())
|
|
436
|
-
AND user_id = ${opts.userId}
|
|
437
|
-
AND session_id = ${opts.sessionId}
|
|
438
|
-
AND agent_id = ${opts.agentId}
|
|
439
|
-
ORDER BY embedding <=> ${embeddingStr}::vector
|
|
440
|
-
LIMIT ${opts.topK}
|
|
441
|
-
`;
|
|
442
|
-
} else if (opts.userId && opts.sessionId) {
|
|
443
|
-
query = prisma.$queryRaw`
|
|
444
|
-
SELECT
|
|
445
|
-
id, content, memory_type as "memoryType", metadata, importance,
|
|
446
|
-
1 - (embedding <=> ${embeddingStr}::vector) as similarity
|
|
447
|
-
FROM memories
|
|
448
|
-
WHERE project_id = ${projectId}
|
|
449
|
-
AND is_active = true
|
|
450
|
-
AND (expires_at IS NULL OR expires_at > NOW())
|
|
451
|
-
AND user_id = ${opts.userId}
|
|
452
|
-
AND session_id = ${opts.sessionId}
|
|
453
|
-
ORDER BY embedding <=> ${embeddingStr}::vector
|
|
454
|
-
LIMIT ${opts.topK}
|
|
455
|
-
`;
|
|
456
|
-
} else if (opts.userId) {
|
|
457
|
-
query = prisma.$queryRaw`
|
|
458
|
-
SELECT
|
|
459
|
-
id, content, memory_type as "memoryType", metadata, importance,
|
|
460
|
-
1 - (embedding <=> ${embeddingStr}::vector) as similarity
|
|
461
|
-
FROM memories
|
|
462
|
-
WHERE project_id = ${projectId}
|
|
463
|
-
AND is_active = true
|
|
464
|
-
AND (expires_at IS NULL OR expires_at > NOW())
|
|
465
|
-
AND user_id = ${opts.userId}
|
|
466
|
-
ORDER BY embedding <=> ${embeddingStr}::vector
|
|
467
|
-
LIMIT ${opts.topK}
|
|
468
|
-
`;
|
|
469
|
-
} else {
|
|
470
|
-
query = prisma.$queryRaw`
|
|
471
|
-
SELECT
|
|
472
|
-
id, content, memory_type as "memoryType", metadata, importance,
|
|
473
|
-
1 - (embedding <=> ${embeddingStr}::vector) as similarity
|
|
474
|
-
FROM memories
|
|
475
|
-
WHERE project_id = ${projectId}
|
|
476
|
-
AND is_active = true
|
|
477
|
-
AND (expires_at IS NULL OR expires_at > NOW())
|
|
478
|
-
ORDER BY embedding <=> ${embeddingStr}::vector
|
|
479
|
-
LIMIT ${opts.topK}
|
|
480
|
-
`;
|
|
476
|
+
async createSharedContext(params) {
|
|
477
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
478
|
+
return this.request("/v1/context/share", {
|
|
479
|
+
method: "POST",
|
|
480
|
+
body: JSON.stringify({ ...params, project })
|
|
481
|
+
});
|
|
481
482
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
483
|
+
async loadSharedContext(shareId) {
|
|
484
|
+
return this.request(`/v1/context/shared/${shareId}`);
|
|
485
|
+
}
|
|
486
|
+
async resumeFromSharedContext(params) {
|
|
487
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
488
|
+
return this.request("/v1/context/resume", {
|
|
489
|
+
method: "POST",
|
|
490
|
+
body: JSON.stringify({ ...params, project })
|
|
491
491
|
});
|
|
492
492
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
chunkType: "memory",
|
|
499
|
-
source: "memory"
|
|
500
|
-
}));
|
|
501
|
-
}
|
|
502
|
-
async function graphSearch(projectId, queryEmbedding, opts) {
|
|
503
|
-
const embeddingStr = `[${queryEmbedding.join(",")}]`;
|
|
504
|
-
const relevantEntities = await prisma.$queryRaw`
|
|
505
|
-
SELECT
|
|
506
|
-
id, name, entity_type as "entityType", description, metadata, source_chunk_id as "sourceChunkId",
|
|
507
|
-
1 - (embedding <=> ${embeddingStr}::vector) as similarity
|
|
508
|
-
FROM entities
|
|
509
|
-
WHERE project_id = ${projectId}
|
|
510
|
-
ORDER BY embedding <=> ${embeddingStr}::vector
|
|
511
|
-
LIMIT 5
|
|
512
|
-
`;
|
|
513
|
-
if (relevantEntities.length === 0) return [];
|
|
514
|
-
const entityIds = relevantEntities.map((e) => e.id);
|
|
515
|
-
const relatedEntities = await prisma.$queryRaw`
|
|
516
|
-
SELECT
|
|
517
|
-
e.id, e.name, e.entity_type as "entityType", e.description, e.metadata, e.source_chunk_id as "sourceChunkId",
|
|
518
|
-
er.relation_type as "relationType", er.weight
|
|
519
|
-
FROM entity_relations er
|
|
520
|
-
INNER JOIN entities e ON er.to_entity_id = e.id
|
|
521
|
-
WHERE er.project_id = ${projectId}
|
|
522
|
-
AND er.from_entity_id = ANY(${entityIds})
|
|
523
|
-
LIMIT ${opts.topK}
|
|
524
|
-
`;
|
|
525
|
-
const chunkIds = [
|
|
526
|
-
...relevantEntities.map((e) => e.sourceChunkId).filter(Boolean),
|
|
527
|
-
...relatedEntities.map((e) => e.sourceChunkId).filter(Boolean)
|
|
528
|
-
];
|
|
529
|
-
if (chunkIds.length === 0) return [];
|
|
530
|
-
const relatedChunks = await prisma.chunk.findMany({
|
|
531
|
-
where: { id: { in: chunkIds } },
|
|
532
|
-
take: opts.topK
|
|
533
|
-
});
|
|
534
|
-
return relatedChunks.map((c) => {
|
|
535
|
-
const entity = relevantEntities.find((e) => e.sourceChunkId === c.id);
|
|
536
|
-
return {
|
|
537
|
-
id: c.id,
|
|
538
|
-
content: c.content,
|
|
539
|
-
score: entity ? entity.similarity * 0.8 : 0.5,
|
|
540
|
-
metadata: {
|
|
541
|
-
...c.metadata,
|
|
542
|
-
entityName: entity?.name,
|
|
543
|
-
entityType: entity?.entityType
|
|
544
|
-
},
|
|
545
|
-
chunkType: c.chunkType || "text",
|
|
546
|
-
source: "graph"
|
|
547
|
-
};
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
function reciprocalRankFusion(results, vectorWeight, bm25Weight, k = 60) {
|
|
551
|
-
const scoreMap = /* @__PURE__ */ new Map();
|
|
552
|
-
const vectorResults = results.filter((r) => r.source === "vector");
|
|
553
|
-
const bm25Results = results.filter((r) => r.source === "bm25");
|
|
554
|
-
const otherResults = results.filter((r) => r.source !== "vector" && r.source !== "bm25");
|
|
555
|
-
vectorResults.forEach((r, rank) => {
|
|
556
|
-
const existing = scoreMap.get(r.id);
|
|
557
|
-
const rrfScore = vectorWeight / (k + rank + 1);
|
|
558
|
-
if (existing) {
|
|
559
|
-
existing.score += rrfScore;
|
|
560
|
-
} else {
|
|
561
|
-
scoreMap.set(r.id, { result: r, score: rrfScore });
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
bm25Results.forEach((r, rank) => {
|
|
565
|
-
const existing = scoreMap.get(r.id);
|
|
566
|
-
const rrfScore = bm25Weight / (k + rank + 1);
|
|
567
|
-
if (existing) {
|
|
568
|
-
existing.score += rrfScore;
|
|
569
|
-
existing.result.source = "hybrid";
|
|
570
|
-
} else {
|
|
571
|
-
scoreMap.set(r.id, { result: { ...r, source: "hybrid" }, score: rrfScore });
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
otherResults.forEach((r) => {
|
|
575
|
-
if (!scoreMap.has(r.id)) {
|
|
576
|
-
scoreMap.set(r.id, { result: r, score: r.score * 0.5 });
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
return Array.from(scoreMap.values()).sort((a, b) => b.score - a.score).map((entry) => ({ ...entry.result, score: entry.score }));
|
|
580
|
-
}
|
|
581
|
-
async function rerankResults(query, results, topK) {
|
|
582
|
-
if (results.length <= 3) return results;
|
|
583
|
-
const candidates = results.slice(0, Math.min(results.length, topK * 3));
|
|
584
|
-
const prompt = `Given the query: "${query}"
|
|
585
|
-
|
|
586
|
-
Rank these ${candidates.length} text passages by relevance (most relevant first). Return ONLY a JSON array of indices (0-based), e.g. [2, 0, 4, 1, 3].
|
|
587
|
-
|
|
588
|
-
${candidates.map((r, i) => `[${i}] ${r.content.slice(0, 300)}`).join("\n\n")}`;
|
|
589
|
-
try {
|
|
590
|
-
const res = await openai2.chat.completions.create({
|
|
591
|
-
model: "gpt-4o-mini",
|
|
592
|
-
// Fixed: was "gpt-4.1-nano" which doesn't exist
|
|
593
|
-
messages: [{ role: "user", content: prompt }],
|
|
594
|
-
temperature: 0,
|
|
595
|
-
max_tokens: 200
|
|
493
|
+
async consolidateMemories(params) {
|
|
494
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
495
|
+
return this.request("/v1/memory/consolidate", {
|
|
496
|
+
method: "POST",
|
|
497
|
+
body: JSON.stringify({ ...params, project })
|
|
596
498
|
});
|
|
597
|
-
const text = res.choices[0]?.message?.content?.trim() || "";
|
|
598
|
-
const match = text.match(/\[[\d,\s]+\]/);
|
|
599
|
-
if (!match) return results;
|
|
600
|
-
const indices = JSON.parse(match[0]);
|
|
601
|
-
const reranked = [];
|
|
602
|
-
for (const idx of indices) {
|
|
603
|
-
if (idx >= 0 && idx < candidates.length) {
|
|
604
|
-
reranked.push({
|
|
605
|
-
...candidates[idx],
|
|
606
|
-
score: 1 - reranked.length * (1 / indices.length)
|
|
607
|
-
// normalize
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
for (const r of results) {
|
|
612
|
-
if (!reranked.find((rr) => rr.id === r.id)) {
|
|
613
|
-
reranked.push(r);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return reranked.slice(0, topK);
|
|
617
|
-
} catch {
|
|
618
|
-
return results.slice(0, topK);
|
|
619
499
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
seen.set(r.id, r);
|
|
627
|
-
}
|
|
500
|
+
async updateImportanceDecay(params) {
|
|
501
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
502
|
+
return this.request("/v1/memory/decay/update", {
|
|
503
|
+
method: "POST",
|
|
504
|
+
body: JSON.stringify({ ...params, project })
|
|
505
|
+
});
|
|
628
506
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (results.length === 0) return "";
|
|
633
|
-
const limit = maxTokens || 8e3;
|
|
634
|
-
let totalTokens = 0;
|
|
635
|
-
const packed = [];
|
|
636
|
-
for (const r of results) {
|
|
637
|
-
const header = buildChunkHeader(r);
|
|
638
|
-
const block = `${header}
|
|
639
|
-
${r.content}
|
|
640
|
-
`;
|
|
641
|
-
const tokens = estimateTokens2(block);
|
|
642
|
-
if (totalTokens + tokens > limit) break;
|
|
643
|
-
packed.push(block);
|
|
644
|
-
totalTokens += tokens;
|
|
507
|
+
async getImportanceStats(project) {
|
|
508
|
+
const resolvedProject = await this.resolveProjectId(this.getRequiredProject(project));
|
|
509
|
+
return this.request(`/v1/memory/decay/stats?project=${resolvedProject}`);
|
|
645
510
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
function buildChunkHeader(r) {
|
|
649
|
-
const parts = [];
|
|
650
|
-
if (r.sourceName) parts.push(`Source: ${r.sourceName}`);
|
|
651
|
-
if (r.documentTitle) parts.push(`Document: ${r.documentTitle}`);
|
|
652
|
-
if (r.metadata?.filePath) parts.push(`File: ${r.metadata.filePath}`);
|
|
653
|
-
if (r.metadata?.startLine) parts.push(`Lines: ${r.metadata.startLine}-${r.metadata.endLine || "?"}`);
|
|
654
|
-
if (r.chunkType && r.chunkType !== "text") parts.push(`Type: ${r.chunkType}`);
|
|
655
|
-
return parts.length > 0 ? `[${parts.join(" | ")}]` : "";
|
|
656
|
-
}
|
|
657
|
-
function estimateTokens2(text) {
|
|
658
|
-
return Math.ceil(text.length / 4);
|
|
659
|
-
}
|
|
660
|
-
async function enrichResults(results) {
|
|
661
|
-
const chunkResults = results.filter((r) => r.source !== "memory");
|
|
662
|
-
if (chunkResults.length === 0) return results;
|
|
663
|
-
const chunkIds = chunkResults.map((r) => r.id);
|
|
664
|
-
if (chunkIds.length === 0) return results;
|
|
665
|
-
const chunkDocs = await prisma.$queryRaw`
|
|
666
|
-
SELECT
|
|
667
|
-
c.id as "chunkId", d.title as "docTitle", s.name as "sourceName"
|
|
668
|
-
FROM chunks c
|
|
669
|
-
INNER JOIN documents d ON c."documentId" = d.id
|
|
670
|
-
INNER JOIN sources s ON d."sourceId" = s.id
|
|
671
|
-
WHERE c.id = ANY(${chunkIds})
|
|
672
|
-
`;
|
|
673
|
-
const enrichMap = new Map(chunkDocs.map((d) => [d.chunkId, d]));
|
|
674
|
-
return results.map((r) => {
|
|
675
|
-
const enrichment = enrichMap.get(r.id);
|
|
676
|
-
if (enrichment) {
|
|
677
|
-
return {
|
|
678
|
-
...r,
|
|
679
|
-
documentTitle: enrichment.docTitle || void 0,
|
|
680
|
-
sourceName: enrichment.sourceName || void 0
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
return r;
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
var MAX_CACHE_PER_PROJECT = 500;
|
|
687
|
-
var lastCacheCleanup = 0;
|
|
688
|
-
var CACHE_CLEANUP_INTERVAL = 6e4;
|
|
689
|
-
function hashQuery(query) {
|
|
690
|
-
return createHash2("sha256").update(query.toLowerCase().trim()).digest("hex");
|
|
691
|
-
}
|
|
692
|
-
async function cleanupQueryCache(projectId) {
|
|
693
|
-
const now = Date.now();
|
|
694
|
-
if (now - lastCacheCleanup < CACHE_CLEANUP_INTERVAL) return;
|
|
695
|
-
lastCacheCleanup = now;
|
|
696
|
-
try {
|
|
697
|
-
const count = await prisma.queryCache.count({ where: { projectId } });
|
|
698
|
-
if (count > MAX_CACHE_PER_PROJECT * 1.5) {
|
|
699
|
-
await prisma.queryCache.deleteMany({
|
|
700
|
-
where: {
|
|
701
|
-
projectId,
|
|
702
|
-
expiresAt: { lt: /* @__PURE__ */ new Date() }
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
const remaining = await prisma.queryCache.count({ where: { projectId } });
|
|
706
|
-
if (remaining > MAX_CACHE_PER_PROJECT) {
|
|
707
|
-
const oldest = await prisma.queryCache.findFirst({
|
|
708
|
-
where: { projectId },
|
|
709
|
-
orderBy: { createdAt: "asc" },
|
|
710
|
-
skip: MAX_CACHE_PER_PROJECT - 1
|
|
711
|
-
});
|
|
712
|
-
if (oldest) {
|
|
713
|
-
await prisma.queryCache.deleteMany({
|
|
714
|
-
where: {
|
|
715
|
-
projectId,
|
|
716
|
-
createdAt: { lte: oldest.createdAt },
|
|
717
|
-
id: { notIn: (await prisma.queryCache.findMany({
|
|
718
|
-
where: { projectId },
|
|
719
|
-
take: MAX_CACHE_PER_PROJECT,
|
|
720
|
-
orderBy: { createdAt: "desc" },
|
|
721
|
-
select: { id: true }
|
|
722
|
-
})).map((x) => x.id) }
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
} catch (error) {
|
|
729
|
-
console.error("Cache cleanup error:", error);
|
|
511
|
+
async getCacheStats() {
|
|
512
|
+
return this.request("/v1/cache/stats");
|
|
730
513
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
projectId,
|
|
737
|
-
queryHash: hash,
|
|
738
|
-
expiresAt: { gt: /* @__PURE__ */ new Date() }
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
if (cached) {
|
|
742
|
-
await prisma.queryCache.update({
|
|
743
|
-
where: { id: cached.id },
|
|
744
|
-
data: { hitCount: { increment: 1 } }
|
|
514
|
+
async warmCache(params) {
|
|
515
|
+
const project = await this.resolveProjectId(this.getRequiredProject(params.project));
|
|
516
|
+
return this.request("/v1/cache/warm", {
|
|
517
|
+
method: "POST",
|
|
518
|
+
body: JSON.stringify({ ...params, project })
|
|
745
519
|
});
|
|
746
|
-
return cached.results;
|
|
747
520
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
521
|
+
async clearCache(params) {
|
|
522
|
+
return this.request("/v1/cache/clear", {
|
|
523
|
+
method: "DELETE",
|
|
524
|
+
body: JSON.stringify(params)
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async getCostSummary(params = {}) {
|
|
528
|
+
const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
|
|
529
|
+
const query = new URLSearchParams({
|
|
530
|
+
...resolvedProject && { project: resolvedProject },
|
|
531
|
+
...params.start_date && { start_date: params.start_date },
|
|
532
|
+
...params.end_date && { end_date: params.end_date }
|
|
533
|
+
});
|
|
534
|
+
return this.request(`/v1/cost/summary?${query}`);
|
|
535
|
+
}
|
|
536
|
+
async getCostBreakdown(params = {}) {
|
|
537
|
+
const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
|
|
538
|
+
const query = new URLSearchParams({
|
|
539
|
+
...resolvedProject && { project: resolvedProject },
|
|
540
|
+
...params.group_by && { group_by: params.group_by },
|
|
541
|
+
...params.start_date && { start_date: params.start_date },
|
|
542
|
+
...params.end_date && { end_date: params.end_date }
|
|
543
|
+
});
|
|
544
|
+
return this.request(`/v1/cost/breakdown?${query}`);
|
|
545
|
+
}
|
|
546
|
+
async getCostSavings(params = {}) {
|
|
547
|
+
const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
|
|
548
|
+
const query = new URLSearchParams({
|
|
549
|
+
...resolvedProject && { project: resolvedProject },
|
|
550
|
+
...params.start_date && { start_date: params.start_date },
|
|
551
|
+
...params.end_date && { end_date: params.end_date }
|
|
552
|
+
});
|
|
553
|
+
return this.request(`/v1/cost/savings?${query}`);
|
|
554
|
+
}
|
|
555
|
+
// Backward-compatible grouped namespaces.
|
|
556
|
+
projects = {
|
|
557
|
+
create: (params) => this.createProject(params),
|
|
558
|
+
list: () => this.listProjects(),
|
|
559
|
+
get: (id) => this.getProject(id),
|
|
560
|
+
delete: (id) => this.deleteProject(id)
|
|
561
|
+
};
|
|
562
|
+
sources = {
|
|
563
|
+
add: (projectId, params) => this.addSource(projectId, params),
|
|
564
|
+
sync: (sourceId) => this.syncSource(sourceId),
|
|
565
|
+
syncSource: (sourceId) => this.syncSource(sourceId)
|
|
566
|
+
};
|
|
567
|
+
memory = {
|
|
568
|
+
add: (params) => this.addMemory(params),
|
|
569
|
+
search: (params) => this.searchMemories(params),
|
|
570
|
+
searchSOTA: (params) => this.searchMemoriesSOTA(params),
|
|
571
|
+
ingestSession: (params) => this.ingestSession(params),
|
|
572
|
+
getSessionMemories: (params) => this.getSessionMemories(params),
|
|
573
|
+
getUserProfile: (params) => this.getUserProfile(params),
|
|
574
|
+
getVersions: (memoryId) => this.getMemoryVersions(memoryId),
|
|
575
|
+
update: (memoryId, params) => this.updateMemory(memoryId, params),
|
|
576
|
+
delete: (memoryId) => this.deleteMemory(memoryId),
|
|
577
|
+
getRelations: (memoryId) => this.getMemoryRelations(memoryId),
|
|
578
|
+
consolidate: (params) => this.consolidateMemories(params),
|
|
579
|
+
updateDecay: (params) => this.updateImportanceDecay(params),
|
|
580
|
+
getImportanceStats: (project) => this.getImportanceStats(project)
|
|
581
|
+
};
|
|
582
|
+
keys = {
|
|
583
|
+
create: (params) => this.createApiKey(params),
|
|
584
|
+
list: () => this.listApiKeys(),
|
|
585
|
+
getUsage: (days) => this.getUsage(days)
|
|
586
|
+
};
|
|
587
|
+
oracle = {
|
|
588
|
+
search: (params) => this.oracleSearch(params)
|
|
589
|
+
};
|
|
590
|
+
context = {
|
|
591
|
+
createShare: (params) => this.createSharedContext(params),
|
|
592
|
+
loadShare: (shareId) => this.loadSharedContext(shareId),
|
|
593
|
+
resumeShare: (params) => this.resumeFromSharedContext(params)
|
|
594
|
+
};
|
|
595
|
+
optimization = {
|
|
596
|
+
getCacheStats: () => this.getCacheStats(),
|
|
597
|
+
warmCache: (params) => this.warmCache(params),
|
|
598
|
+
clearCache: (params) => this.clearCache(params),
|
|
599
|
+
getCostSummary: (params) => this.getCostSummary(params),
|
|
600
|
+
getCostBreakdown: (params) => this.getCostBreakdown(params),
|
|
601
|
+
getCostSavings: (params) => this.getCostSavings(params)
|
|
602
|
+
};
|
|
603
|
+
};
|
|
775
604
|
|
|
776
605
|
// ../src/mcp/server.ts
|
|
777
|
-
var
|
|
606
|
+
var API_KEY = process.env.WHISPER_API_KEY || "";
|
|
607
|
+
var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
|
|
608
|
+
var BASE_URL = process.env.WHISPER_BASE_URL;
|
|
609
|
+
if (!API_KEY) {
|
|
610
|
+
console.error("Error: WHISPER_API_KEY environment variable is required");
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
var whisper = new WhisperContext({
|
|
614
|
+
apiKey: API_KEY,
|
|
615
|
+
project: DEFAULT_PROJECT,
|
|
616
|
+
...BASE_URL && { baseUrl: BASE_URL }
|
|
617
|
+
});
|
|
778
618
|
var server = new McpServer({
|
|
779
619
|
name: "whisper-context",
|
|
780
|
-
version: "0.
|
|
620
|
+
version: "0.2.8"
|
|
781
621
|
});
|
|
782
|
-
async function resolveProject(name) {
|
|
783
|
-
const proj = await prisma.project.findFirst({
|
|
784
|
-
where: {
|
|
785
|
-
orgId: ORG_ID,
|
|
786
|
-
OR: [
|
|
787
|
-
{ name },
|
|
788
|
-
{ slug: name }
|
|
789
|
-
]
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
return proj;
|
|
793
|
-
}
|
|
794
622
|
server.tool(
|
|
795
623
|
"query_context",
|
|
796
624
|
"Search your knowledge base for relevant context. Returns packed context ready for LLM consumption. Supports hybrid vector+keyword search, memory inclusion, and knowledge graph traversal.",
|
|
797
625
|
{
|
|
798
|
-
project: z.string().describe("Project name or slug"),
|
|
626
|
+
project: z.string().optional().describe("Project name or slug (optional if WHISPER_PROJECT is set)"),
|
|
799
627
|
query: z.string().describe("What are you looking for?"),
|
|
800
628
|
top_k: z.number().optional().default(10).describe("Number of results"),
|
|
801
629
|
chunk_types: z.array(z.string()).optional().describe("Filter: code, function, class, documentation, api_spec, schema, config, text"),
|
|
@@ -806,93 +634,89 @@ server.tool(
|
|
|
806
634
|
max_tokens: z.number().optional().describe("Max tokens for packed context")
|
|
807
635
|
},
|
|
808
636
|
async ({ project, query, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens }) => {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const header = `Found ${response.meta.totalResults} results (${response.meta.latencyMs}ms${response.meta.cacheHit ? ", cached" : ""}):
|
|
637
|
+
try {
|
|
638
|
+
const response = await whisper.query({
|
|
639
|
+
project,
|
|
640
|
+
query,
|
|
641
|
+
top_k,
|
|
642
|
+
chunk_types,
|
|
643
|
+
include_memories,
|
|
644
|
+
include_graph,
|
|
645
|
+
user_id,
|
|
646
|
+
session_id,
|
|
647
|
+
max_tokens
|
|
648
|
+
});
|
|
649
|
+
if (response.results.length === 0) {
|
|
650
|
+
return { content: [{ type: "text", text: "No relevant context found." }] };
|
|
651
|
+
}
|
|
652
|
+
const header = `Found ${response.meta.total} results (${response.meta.latency_ms}ms${response.meta.cache_hit ? ", cached" : ""}):
|
|
826
653
|
|
|
827
654
|
`;
|
|
828
|
-
|
|
655
|
+
return { content: [{ type: "text", text: header + response.context }] };
|
|
656
|
+
} catch (error) {
|
|
657
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
658
|
+
}
|
|
829
659
|
}
|
|
830
660
|
);
|
|
831
661
|
server.tool(
|
|
832
662
|
"add_memory",
|
|
833
663
|
"Store a memory (fact, preference, decision) that persists across conversations. Memories can be scoped to a user, session, or agent.",
|
|
834
664
|
{
|
|
835
|
-
project: z.string().describe("Project name or slug"),
|
|
665
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
836
666
|
content: z.string().describe("The memory content to store"),
|
|
837
|
-
memory_type: z.enum(["factual", "
|
|
667
|
+
memory_type: z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"]).optional().default("factual"),
|
|
838
668
|
user_id: z.string().optional().describe("User this memory belongs to"),
|
|
839
669
|
session_id: z.string().optional().describe("Session scope"),
|
|
840
670
|
agent_id: z.string().optional().describe("Agent scope"),
|
|
841
671
|
importance: z.number().optional().default(0.5).describe("Importance 0-1")
|
|
842
672
|
},
|
|
843
673
|
async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const memory = await prisma.memory.create({
|
|
848
|
-
data: {
|
|
849
|
-
projectId: proj.id,
|
|
674
|
+
try {
|
|
675
|
+
const result = await whisper.addMemory({
|
|
676
|
+
project,
|
|
850
677
|
content,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
importance
|
|
856
|
-
|
|
857
|
-
}
|
|
858
|
-
})
|
|
859
|
-
|
|
678
|
+
memory_type,
|
|
679
|
+
user_id,
|
|
680
|
+
session_id,
|
|
681
|
+
agent_id,
|
|
682
|
+
importance
|
|
683
|
+
});
|
|
684
|
+
return { content: [{ type: "text", text: `Memory stored (id: ${result.id}, type: ${memory_type}).` }] };
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
687
|
+
}
|
|
860
688
|
}
|
|
861
689
|
);
|
|
862
690
|
server.tool(
|
|
863
691
|
"search_memories",
|
|
864
692
|
"Search stored memories by semantic similarity. Recall facts, preferences, past decisions from previous interactions.",
|
|
865
693
|
{
|
|
866
|
-
project: z.string().describe("Project name or slug"),
|
|
694
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
867
695
|
query: z.string().describe("What to search for"),
|
|
868
696
|
user_id: z.string().optional().describe("Filter by user"),
|
|
869
697
|
session_id: z.string().optional().describe("Filter by session"),
|
|
870
|
-
top_k: z.number().optional().default(10).describe("Number of results")
|
|
698
|
+
top_k: z.number().optional().default(10).describe("Number of results"),
|
|
699
|
+
memory_types: z.array(z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"])).optional()
|
|
871
700
|
},
|
|
872
|
-
async ({ project, query, user_id, session_id, top_k }) => {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
1 - (embedding <=> '${JSON.stringify(queryEmbedding)}'::vector) as similarity
|
|
887
|
-
FROM memories
|
|
888
|
-
WHERE ${whereClause}
|
|
889
|
-
ORDER BY embedding <=> '${JSON.stringify(queryEmbedding)}'::vector
|
|
890
|
-
LIMIT ${top_k}
|
|
891
|
-
`);
|
|
892
|
-
if (results.length === 0) return { content: [{ type: "text", text: "No memories found." }] };
|
|
893
|
-
const text = results.map((r, i) => `${i + 1}. [${r.memoryType}, importance: ${r.importance}, score: ${r.similarity.toFixed(3)}]
|
|
701
|
+
async ({ project, query, user_id, session_id, top_k, memory_types }) => {
|
|
702
|
+
try {
|
|
703
|
+
const results = await whisper.searchMemoriesSOTA({
|
|
704
|
+
project,
|
|
705
|
+
query,
|
|
706
|
+
user_id,
|
|
707
|
+
session_id,
|
|
708
|
+
top_k,
|
|
709
|
+
memory_types
|
|
710
|
+
});
|
|
711
|
+
if (!results.memories || results.memories.length === 0) {
|
|
712
|
+
return { content: [{ type: "text", text: "No memories found." }] };
|
|
713
|
+
}
|
|
714
|
+
const text = results.memories.map((r, i) => `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
|
|
894
715
|
${r.content}`).join("\n\n");
|
|
895
|
-
|
|
716
|
+
return { content: [{ type: "text", text }] };
|
|
717
|
+
} catch (error) {
|
|
718
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
719
|
+
}
|
|
896
720
|
}
|
|
897
721
|
);
|
|
898
722
|
server.tool(
|
|
@@ -900,139 +724,56 @@ server.tool(
|
|
|
900
724
|
"List all available context projects.",
|
|
901
725
|
{},
|
|
902
726
|
async () => {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
727
|
+
try {
|
|
728
|
+
const { projects } = await whisper.listProjects();
|
|
729
|
+
const text = projects.length === 0 ? "No projects found." : projects.map((p) => `- ${p.name} (${p.slug})${p.description ? `: ${p.description}` : ""}`).join("\n");
|
|
730
|
+
return { content: [{ type: "text", text }] };
|
|
731
|
+
} catch (error) {
|
|
732
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
733
|
+
}
|
|
908
734
|
}
|
|
909
735
|
);
|
|
910
736
|
server.tool(
|
|
911
737
|
"list_sources",
|
|
912
738
|
"List all data sources connected to a project.",
|
|
913
|
-
{ project: z.string().describe("Project name or slug") },
|
|
739
|
+
{ project: z.string().optional().describe("Project name or slug") },
|
|
914
740
|
async ({ project }) => {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
741
|
+
try {
|
|
742
|
+
const projectData = await whisper.getProject(project || DEFAULT_PROJECT);
|
|
743
|
+
const srcs = projectData.sources || [];
|
|
744
|
+
const text = srcs.length === 0 ? "No sources connected." : srcs.map((s) => `- ${s.name} (${s.connectorType}) \u2014 ${s.status}`).join("\n");
|
|
745
|
+
return { content: [{ type: "text", text }] };
|
|
746
|
+
} catch (error) {
|
|
747
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
748
|
+
}
|
|
922
749
|
}
|
|
923
750
|
);
|
|
924
751
|
server.tool(
|
|
925
752
|
"add_context",
|
|
926
753
|
"Add text content to a project's knowledge base.",
|
|
927
754
|
{
|
|
928
|
-
project: z.string().describe("Project name or slug"),
|
|
755
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
929
756
|
title: z.string().describe("Title for this content"),
|
|
930
757
|
content: z.string().describe("The text content to index")
|
|
931
758
|
},
|
|
932
759
|
async ({ project, title, content }) => {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
connectorType: "custom",
|
|
939
|
-
name: "mcp-ingest"
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
if (!directSource) {
|
|
943
|
-
directSource = await prisma.source.create({
|
|
944
|
-
data: {
|
|
945
|
-
orgId: ORG_ID,
|
|
946
|
-
projectId: proj.id,
|
|
947
|
-
name: "mcp-ingest",
|
|
948
|
-
type: "custom",
|
|
949
|
-
connectorType: "custom",
|
|
950
|
-
config: {},
|
|
951
|
-
status: "READY"
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
await ingestDocument({ sourceId: directSource.id, projectId: proj.id, externalId: `mcp-${title}`, title, content });
|
|
956
|
-
return { content: [{ type: "text", text: `Indexed "${title}" (${content.length} chars) into '${project}'.` }] };
|
|
957
|
-
}
|
|
958
|
-
);
|
|
959
|
-
server.tool(
|
|
960
|
-
"track_conversation",
|
|
961
|
-
"Add a message to a conversation. Creates the conversation if it doesn't exist.",
|
|
962
|
-
{
|
|
963
|
-
project: z.string().describe("Project name or slug"),
|
|
964
|
-
session_id: z.string().describe("Unique session identifier"),
|
|
965
|
-
role: z.enum(["user", "assistant", "system", "tool"]),
|
|
966
|
-
content: z.string().describe("Message content"),
|
|
967
|
-
user_id: z.string().optional().describe("User identifier")
|
|
968
|
-
},
|
|
969
|
-
async ({ project, session_id, role, content, user_id }) => {
|
|
970
|
-
const proj = await resolveProject(project);
|
|
971
|
-
if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
|
|
972
|
-
let conv = await prisma.session.findFirst({
|
|
973
|
-
where: {
|
|
974
|
-
projectId: proj.id,
|
|
975
|
-
sessionId: session_id
|
|
976
|
-
}
|
|
977
|
-
});
|
|
978
|
-
if (!conv) {
|
|
979
|
-
conv = await prisma.session.create({
|
|
980
|
-
data: {
|
|
981
|
-
projectId: proj.id,
|
|
982
|
-
sessionId: session_id,
|
|
983
|
-
userId: user_id
|
|
984
|
-
}
|
|
760
|
+
try {
|
|
761
|
+
await whisper.addContext({
|
|
762
|
+
project,
|
|
763
|
+
title,
|
|
764
|
+
content
|
|
985
765
|
});
|
|
766
|
+
return { content: [{ type: "text", text: `Indexed "${title}" (${content.length} chars).` }] };
|
|
767
|
+
} catch (error) {
|
|
768
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
986
769
|
}
|
|
987
|
-
await prisma.message.create({
|
|
988
|
-
data: {
|
|
989
|
-
conversationId: conv.id,
|
|
990
|
-
role,
|
|
991
|
-
content
|
|
992
|
-
}
|
|
993
|
-
});
|
|
994
|
-
await prisma.session.update({
|
|
995
|
-
where: { id: conv.id },
|
|
996
|
-
data: {
|
|
997
|
-
messageCount: { increment: 1 },
|
|
998
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
return { content: [{ type: "text", text: `Message added (session: ${session_id}).` }] };
|
|
1002
|
-
}
|
|
1003
|
-
);
|
|
1004
|
-
server.tool(
|
|
1005
|
-
"get_conversation",
|
|
1006
|
-
"Retrieve conversation history for a session.",
|
|
1007
|
-
{
|
|
1008
|
-
project: z.string().describe("Project name or slug"),
|
|
1009
|
-
session_id: z.string().describe("Session identifier"),
|
|
1010
|
-
limit: z.number().optional().default(50)
|
|
1011
|
-
},
|
|
1012
|
-
async ({ project, session_id, limit }) => {
|
|
1013
|
-
const proj = await resolveProject(project);
|
|
1014
|
-
if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
|
|
1015
|
-
const conv = await prisma.session.findFirst({
|
|
1016
|
-
where: {
|
|
1017
|
-
projectId: proj.id,
|
|
1018
|
-
sessionId: session_id
|
|
1019
|
-
}
|
|
1020
|
-
});
|
|
1021
|
-
if (!conv) return { content: [{ type: "text", text: "No conversation found for this session." }] };
|
|
1022
|
-
const msgs = await prisma.message.findMany({
|
|
1023
|
-
where: { conversationId: conv.id },
|
|
1024
|
-
orderBy: { createdAt: "asc" },
|
|
1025
|
-
take: limit
|
|
1026
|
-
});
|
|
1027
|
-
const text = msgs.map((m) => `[${m.role}]: ${m.content}`).join("\n\n");
|
|
1028
|
-
return { content: [{ type: "text", text: text || "No messages yet." }] };
|
|
1029
770
|
}
|
|
1030
771
|
);
|
|
1031
772
|
server.tool(
|
|
1032
773
|
"memory_search_sota",
|
|
1033
774
|
"SOTA memory search with temporal reasoning and relation graphs. Searches memories with support for temporal queries ('what did I say yesterday?'), type filtering, and knowledge graph traversal.",
|
|
1034
775
|
{
|
|
1035
|
-
project: z.string().describe("Project name or slug"),
|
|
776
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
1036
777
|
query: z.string().describe("Search query (supports temporal: 'yesterday', 'last week')"),
|
|
1037
778
|
user_id: z.string().optional().describe("Filter by user"),
|
|
1038
779
|
session_id: z.string().optional().describe("Filter by session"),
|
|
@@ -1042,43 +783,42 @@ server.tool(
|
|
|
1042
783
|
include_relations: z.boolean().optional().default(true).describe("Include related memories via knowledge graph")
|
|
1043
784
|
},
|
|
1044
785
|
async ({ project, query, user_id, session_id, question_date, memory_types, top_k, include_relations }) => {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
`;
|
|
1062
|
-
line += ` ${r.memory.content}
|
|
786
|
+
try {
|
|
787
|
+
const results = await whisper.searchMemoriesSOTA({
|
|
788
|
+
project,
|
|
789
|
+
query,
|
|
790
|
+
user_id,
|
|
791
|
+
session_id,
|
|
792
|
+
question_date,
|
|
793
|
+
memory_types,
|
|
794
|
+
top_k,
|
|
795
|
+
include_relations
|
|
796
|
+
});
|
|
797
|
+
if (!results.memories || results.memories.length === 0) {
|
|
798
|
+
return { content: [{ type: "text", text: "No memories found." }] };
|
|
799
|
+
}
|
|
800
|
+
const text = results.memories.map((r, i) => {
|
|
801
|
+
let line = `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
|
|
1063
802
|
`;
|
|
1064
|
-
|
|
1065
|
-
line += ` Event: ${r.memory.temporal.eventDate.toISOString().split("T")[0]}
|
|
803
|
+
line += ` ${r.content}
|
|
1066
804
|
`;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
line += ` Relations: ${r.relations.map((rel) => rel.relationType).join(", ")}
|
|
805
|
+
if (r.event_date) {
|
|
806
|
+
line += ` Event: ${new Date(r.event_date).toISOString().split("T")[0]}
|
|
1070
807
|
`;
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
808
|
+
}
|
|
809
|
+
return line;
|
|
810
|
+
}).join("\n");
|
|
811
|
+
return { content: [{ type: "text", text }] };
|
|
812
|
+
} catch (error) {
|
|
813
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
814
|
+
}
|
|
1075
815
|
}
|
|
1076
816
|
);
|
|
1077
817
|
server.tool(
|
|
1078
818
|
"ingest_conversation",
|
|
1079
819
|
"Extract memories from a conversation session. Automatically handles disambiguation, temporal grounding, and relation detection.",
|
|
1080
820
|
{
|
|
1081
|
-
project: z.string().describe("Project name or slug"),
|
|
821
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
1082
822
|
session_id: z.string().describe("Session identifier"),
|
|
1083
823
|
user_id: z.string().optional().describe("User identifier"),
|
|
1084
824
|
messages: z.array(z.object({
|
|
@@ -1088,75 +828,73 @@ server.tool(
|
|
|
1088
828
|
})).describe("Array of conversation messages with timestamps")
|
|
1089
829
|
},
|
|
1090
830
|
async ({ project, session_id, user_id, messages }) => {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
return {
|
|
1106
|
-
content: [{
|
|
1107
|
-
type: "text",
|
|
1108
|
-
text: `Processed ${messages.length} messages:
|
|
1109
|
-
- Created ${result.memoriesCreated} memories
|
|
1110
|
-
- Detected ${result.relationsCreated} relations
|
|
1111
|
-
- Updated ${result.memoriesInvalidated} outdated memories` + (result.errors && result.errors.length > 0 ? `
|
|
831
|
+
try {
|
|
832
|
+
const result = await whisper.ingestSession({
|
|
833
|
+
project,
|
|
834
|
+
session_id,
|
|
835
|
+
user_id,
|
|
836
|
+
messages
|
|
837
|
+
});
|
|
838
|
+
return {
|
|
839
|
+
content: [{
|
|
840
|
+
type: "text",
|
|
841
|
+
text: `Processed ${messages.length} messages:
|
|
842
|
+
- Created ${result.memories_created} memories
|
|
843
|
+
- Detected ${result.relations_created} relations
|
|
844
|
+
- Updated ${result.memories_invalidated} outdated memories` + (result.errors && result.errors.length > 0 ? `
|
|
1112
845
|
- Errors: ${result.errors.join(", ")}` : "")
|
|
1113
|
-
|
|
1114
|
-
|
|
846
|
+
}]
|
|
847
|
+
};
|
|
848
|
+
} catch (error) {
|
|
849
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
850
|
+
}
|
|
1115
851
|
}
|
|
1116
852
|
);
|
|
1117
853
|
server.tool(
|
|
1118
854
|
"oracle_search",
|
|
1119
855
|
"Oracle Research Mode - Tree-guided document navigation with multi-step reasoning. More precise than standard search, especially for bleeding-edge features.",
|
|
1120
856
|
{
|
|
1121
|
-
project: z.string().describe("Project name or slug"),
|
|
857
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
1122
858
|
query: z.string().describe("Research question"),
|
|
1123
859
|
mode: z.enum(["search", "research"]).optional().default("search").describe("'search' for tree-guided, 'research' for multi-step reasoning"),
|
|
1124
860
|
max_results: z.number().optional().default(5),
|
|
1125
861
|
max_steps: z.number().optional().default(5).describe("For research mode: max reasoning steps")
|
|
1126
862
|
},
|
|
1127
863
|
async ({ project, query, mode, max_results, max_steps }) => {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
maxSteps: max_steps
|
|
864
|
+
try {
|
|
865
|
+
const results = await whisper.oracleSearch({
|
|
866
|
+
project,
|
|
867
|
+
query,
|
|
868
|
+
mode,
|
|
869
|
+
max_results,
|
|
870
|
+
max_steps
|
|
1136
871
|
});
|
|
1137
|
-
|
|
872
|
+
if (mode === "research" && results.answer) {
|
|
873
|
+
let text = `Answer: ${results.answer}
|
|
1138
874
|
|
|
1139
875
|
Reasoning Steps:
|
|
1140
876
|
`;
|
|
1141
|
-
|
|
1142
|
-
|
|
877
|
+
if (results.steps) {
|
|
878
|
+
results.steps.forEach((step, i) => {
|
|
879
|
+
text += `${i + 1}. Query: ${step.query}
|
|
1143
880
|
Reasoning: ${step.reasoning}
|
|
1144
|
-
Results: ${step.results
|
|
881
|
+
Results: ${step.results?.length || 0} items
|
|
1145
882
|
`;
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
const text = results.map(
|
|
1156
|
-
(r, i) => `${i + 1}. [${r.path}] (relevance: ${r.relevance.toFixed(3)})
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
return { content: [{ type: "text", text }] };
|
|
886
|
+
} else {
|
|
887
|
+
if (!results.results || results.results.length === 0) {
|
|
888
|
+
return { content: [{ type: "text", text: "No results found." }] };
|
|
889
|
+
}
|
|
890
|
+
const text = results.results.map(
|
|
891
|
+
(r, i) => `${i + 1}. [${r.path || r.source}] (relevance: ${r.relevance?.toFixed(3) || r.score?.toFixed(3) || "N/A"})
|
|
1157
892
|
${r.content.slice(0, 200)}...`
|
|
1158
|
-
|
|
1159
|
-
|
|
893
|
+
).join("\n\n");
|
|
894
|
+
return { content: [{ type: "text", text }] };
|
|
895
|
+
}
|
|
896
|
+
} catch (error) {
|
|
897
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
1160
898
|
}
|
|
1161
899
|
}
|
|
1162
900
|
);
|
|
@@ -1164,7 +902,7 @@ server.tool(
|
|
|
1164
902
|
"autosubscribe_dependencies",
|
|
1165
903
|
"Automatically index a project's dependencies (package.json, requirements.txt, etc.). Resolves docs URLs and indexes documentation.",
|
|
1166
904
|
{
|
|
1167
|
-
project: z.string().describe("Project name or slug"),
|
|
905
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
1168
906
|
source_type: z.enum(["github", "local"]).describe("Source location"),
|
|
1169
907
|
github_owner: z.string().optional().describe("For GitHub: owner/org name"),
|
|
1170
908
|
github_repo: z.string().optional().describe("For GitHub: repository name"),
|
|
@@ -1173,104 +911,100 @@ server.tool(
|
|
|
1173
911
|
index_limit: z.number().optional().default(20).describe("Max dependencies to index")
|
|
1174
912
|
},
|
|
1175
913
|
async ({ project, source_type, github_owner, github_repo, local_path, dependency_file, index_limit }) => {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
content: [{
|
|
1187
|
-
type: "text",
|
|
1188
|
-
text: `Autosubscribe completed:
|
|
914
|
+
try {
|
|
915
|
+
const result = await whisper.autosubscribe({
|
|
916
|
+
project,
|
|
917
|
+
source: source_type === "github" ? { type: "github", owner: github_owner, repo: github_repo } : { type: "local", path: local_path },
|
|
918
|
+
index_limit
|
|
919
|
+
});
|
|
920
|
+
return {
|
|
921
|
+
content: [{
|
|
922
|
+
type: "text",
|
|
923
|
+
text: `Autosubscribe completed:
|
|
1189
924
|
- Discovered: ${result.discovered} dependencies
|
|
1190
925
|
- Indexed: ${result.indexed} successfully
|
|
1191
|
-
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
926
|
+
- Errors: ${result.errors?.length || 0}`
|
|
927
|
+
}]
|
|
928
|
+
};
|
|
929
|
+
} catch (error) {
|
|
930
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
931
|
+
}
|
|
1195
932
|
}
|
|
1196
933
|
);
|
|
1197
934
|
server.tool(
|
|
1198
935
|
"share_context",
|
|
1199
936
|
"Create a shareable snapshot of a conversation with memories. Returns a URL that can be shared or resumed later.",
|
|
1200
937
|
{
|
|
1201
|
-
project: z.string().describe("Project name or slug"),
|
|
938
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
1202
939
|
session_id: z.string().describe("Session to share"),
|
|
1203
940
|
title: z.string().optional().describe("Title for the shared context"),
|
|
1204
941
|
expiry_days: z.number().optional().default(30).describe("Days until expiry")
|
|
1205
942
|
},
|
|
1206
943
|
async ({ project, session_id, title, expiry_days }) => {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
-
|
|
1222
|
-
- Memories: ${result.memories?.length || 0}
|
|
1223
|
-
- Messages: ${result.messages?.length || 0}
|
|
1224
|
-
- Expires: ${result.expiresAt?.toISOString() || "Never"}
|
|
944
|
+
try {
|
|
945
|
+
const result = await whisper.createSharedContext({
|
|
946
|
+
project,
|
|
947
|
+
session_id,
|
|
948
|
+
title,
|
|
949
|
+
expiry_days
|
|
950
|
+
});
|
|
951
|
+
return {
|
|
952
|
+
content: [{
|
|
953
|
+
type: "text",
|
|
954
|
+
text: `Shared context created:
|
|
955
|
+
- Share ID: ${result.share_id}
|
|
956
|
+
- Memories: ${result.memories_count || 0}
|
|
957
|
+
- Messages: ${result.messages_count || 0}
|
|
958
|
+
- Expires: ${result.expires_at || "Never"}
|
|
1225
959
|
|
|
1226
|
-
Share URL: ${result.
|
|
1227
|
-
|
|
1228
|
-
|
|
960
|
+
Share URL: ${result.share_url}`
|
|
961
|
+
}]
|
|
962
|
+
};
|
|
963
|
+
} catch (error) {
|
|
964
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
965
|
+
}
|
|
1229
966
|
}
|
|
1230
967
|
);
|
|
1231
968
|
server.tool(
|
|
1232
969
|
"consolidate_memories",
|
|
1233
970
|
"Find and merge duplicate memories to reduce bloat. Uses vector similarity + LLM merging.",
|
|
1234
971
|
{
|
|
1235
|
-
project: z.string().describe("Project name or slug"),
|
|
972
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
1236
973
|
similarity_threshold: z.number().optional().default(0.95).describe("Similarity threshold (0-1)"),
|
|
1237
974
|
dry_run: z.boolean().optional().default(false).describe("Preview without merging")
|
|
1238
975
|
},
|
|
1239
976
|
async ({ project, similarity_threshold, dry_run }) => {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
projectId: proj.id,
|
|
1246
|
-
similarityThreshold: similarity_threshold
|
|
977
|
+
try {
|
|
978
|
+
const result = await whisper.consolidateMemories({
|
|
979
|
+
project,
|
|
980
|
+
similarity_threshold,
|
|
981
|
+
dry_run
|
|
1247
982
|
});
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
983
|
+
if (dry_run && result.clusters) {
|
|
984
|
+
const totalDuplicates = result.clusters.reduce((sum, c) => sum + (c.duplicates?.length || 0), 0);
|
|
985
|
+
return {
|
|
986
|
+
content: [{
|
|
987
|
+
type: "text",
|
|
988
|
+
text: `Found ${result.clusters.length} duplicate clusters:
|
|
1253
989
|
- Total duplicates: ${totalDuplicates}
|
|
1254
990
|
- Estimated savings: ${totalDuplicates} memories
|
|
1255
991
|
|
|
1256
992
|
Run without dry_run to merge.`
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
}]
|
|
1273
|
-
};
|
|
993
|
+
}]
|
|
994
|
+
};
|
|
995
|
+
} else {
|
|
996
|
+
return {
|
|
997
|
+
content: [{
|
|
998
|
+
type: "text",
|
|
999
|
+
text: `Consolidation complete:
|
|
1000
|
+
- Clusters found: ${result.clusters_found || 0}
|
|
1001
|
+
- Memories merged: ${result.memories_merged || 0}
|
|
1002
|
+
- Memories deactivated: ${result.memories_deactivated || 0}`
|
|
1003
|
+
}]
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
1274
1008
|
}
|
|
1275
1009
|
}
|
|
1276
1010
|
);
|
|
@@ -1282,48 +1016,39 @@ server.tool(
|
|
|
1282
1016
|
days: z.number().optional().default(30).describe("Time period in days")
|
|
1283
1017
|
},
|
|
1284
1018
|
async ({ project, days }) => {
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
since: startDate
|
|
1300
|
-
});
|
|
1301
|
-
let text = `Cost Summary (last ${days} days):
|
|
1019
|
+
try {
|
|
1020
|
+
const endDate = /* @__PURE__ */ new Date();
|
|
1021
|
+
const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1022
|
+
const summary = await whisper.getCostSummary({
|
|
1023
|
+
project,
|
|
1024
|
+
start_date: startDate.toISOString(),
|
|
1025
|
+
end_date: endDate.toISOString()
|
|
1026
|
+
});
|
|
1027
|
+
const savings = await whisper.getCostSavings({
|
|
1028
|
+
project,
|
|
1029
|
+
start_date: startDate.toISOString(),
|
|
1030
|
+
end_date: endDate.toISOString()
|
|
1031
|
+
});
|
|
1032
|
+
let text = `Cost Summary (last ${days} days):
|
|
1302
1033
|
|
|
1303
1034
|
`;
|
|
1304
|
-
|
|
1035
|
+
text += `Total Cost: $${savings.actual_cost?.toFixed(2) || "0.00"}
|
|
1305
1036
|
`;
|
|
1306
|
-
|
|
1307
|
-
`;
|
|
1308
|
-
text += `Avg Cost/Request: $${(savings.actualCost / (Object.values(summary).reduce((sum, s) => sum + s.calls, 0) || 1)).toFixed(4)}
|
|
1037
|
+
text += `Total Requests: ${summary.total_requests || 0}
|
|
1309
1038
|
|
|
1310
1039
|
`;
|
|
1311
|
-
|
|
1312
|
-
`;
|
|
1313
|
-
Object.entries(summary).forEach(([model, stats]) => {
|
|
1314
|
-
text += `- ${model}: $${stats.totalCost.toFixed(2)} (${stats.calls} calls)
|
|
1040
|
+
text += `Savings vs Always-Opus:
|
|
1315
1041
|
`;
|
|
1316
|
-
|
|
1317
|
-
text += `
|
|
1318
|
-
Savings vs Always-Opus:
|
|
1319
|
-
`;
|
|
1320
|
-
text += `- Actual: $${savings.actualCost.toFixed(2)}
|
|
1042
|
+
text += `- Actual: $${savings.actual_cost?.toFixed(2) || "0.00"}
|
|
1321
1043
|
`;
|
|
1322
|
-
|
|
1044
|
+
text += `- Opus-only: $${savings.opus_cost?.toFixed(2) || "0.00"}
|
|
1323
1045
|
`;
|
|
1324
|
-
|
|
1046
|
+
text += `- Saved: $${savings.savings?.toFixed(2) || "0.00"} (${savings.savings_percent?.toFixed(1) || "0"}%)
|
|
1325
1047
|
`;
|
|
1326
|
-
|
|
1048
|
+
return { content: [{ type: "text", text }] };
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
1051
|
+
}
|
|
1327
1052
|
}
|
|
1328
1053
|
);
|
|
1329
1054
|
async function main() {
|